api types; navbar style refactoring; user themes
This commit is contained in:
+2
-1
@@ -43,7 +43,8 @@
|
|||||||
"not op_mini all"
|
"not op_mini all"
|
||||||
],
|
],
|
||||||
"development": [
|
"development": [
|
||||||
"last 1 chrome version"
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -1,6 +1,6 @@
|
|||||||
const baseUrl = process.env.NODE_ENV === "development" ? 'http://localhost:8080/v1' : 'https://api.unmei.space/v1';
|
const baseUrl = process.env.NODE_ENV === "development" ? 'http://localhost:8080/v1' : 'https://api.unmei.space/v1';
|
||||||
|
|
||||||
const response = async (url: string, method: string, body?: string | FormData) => {
|
const request: ApiRequest = 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);
|
||||||
@@ -18,17 +18,17 @@ const response = async (url: string, method: string, body?: string | FormData) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
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, request(url, method, body)), 1500);
|
||||||
});
|
});
|
||||||
if(json?.error) return Promise.reject(json.error_data);
|
if(json?.error) return Promise.reject(json.error_data);
|
||||||
if(!res.ok) return Promise.reject(res.statusText);
|
if(!res.ok) return Promise.reject(res.statusText);
|
||||||
return Promise.resolve(json.data);
|
return Promise.resolve(json.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const get = (url: string, data?: object) => response(url, 'GET', JSON.stringify(data));
|
const get: Get = (url: string, data?: object) => request(url, 'GET', JSON.stringify(data));
|
||||||
const post = (url: string, data?: object | FormData) => response(url, 'POST', data instanceof FormData ? data : JSON.stringify(data));
|
const post: Post = (url: string, data?: object | FormData) => request(url, 'POST', data instanceof FormData ? data : JSON.stringify(data));
|
||||||
const put = (url: string, data?: object) => response(url, 'PUT', JSON.stringify(data));
|
const put: Put = (url: string, data?: object) => request(url, 'PUT', JSON.stringify(data));
|
||||||
const del = (url: string, data?: object) => response(url, 'DELETE', JSON.stringify(data));
|
const del: Delete = (url: string, data?: object) => request(url, 'DELETE', JSON.stringify(data));
|
||||||
|
|
||||||
const TranslateStatus: { [id: string]: string } = {
|
const TranslateStatus: { [id: string]: string } = {
|
||||||
planned: 'Запланировано',
|
planned: 'Запланировано',
|
||||||
@@ -39,6 +39,6 @@ const TranslateStatus: { [id: string]: string } = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getVersion = () => get('version');
|
export const getVersion = () => get('version');
|
||||||
export const version = '0.10';
|
export const version = '0.10b';
|
||||||
|
|
||||||
export { get, post, put, del, TranslateStatus };
|
export { get, post, put, del, TranslateStatus };
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
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: Restore = (token: string, new_password: string) => post('auth/restore', { token, new_password });
|
||||||
export const registerUser = (login: string, password: string, email: string, recaptcha: string) => post('auth/register', { login, password, email, recaptcha });
|
export const registerUser: Register = (login: string, password: string, email: string, recaptcha: string) => post('auth/register', { login, password, email, recaptcha });
|
||||||
export const login = (login: string, password: string, recaptcha: string) => post('auth/login', { login, password, recaptcha });
|
export const login: Login = (login: string, password: string, recaptcha: string) => post('auth/login', { login, password, recaptcha });
|
||||||
export const userLogout = () => post('auth/logout');
|
export const userLogout: Logout = () => post('auth/logout');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {get} from "./api";
|
import {get} from "./api";
|
||||||
|
|
||||||
export const fetchChar = (id: number) => get(`characters/${id}`);
|
export const fetchChar: FetchChar = (id: number) => get(`characters/${id}`);
|
||||||
export const fetchCharNovels = (id: number) => get(`characters/${id}/novels`);
|
export const fetchCharNovels: FetchCharNovels = (id: number) => get(`characters/${id}/novels`);
|
||||||
|
|||||||
+10
-9
@@ -1,18 +1,19 @@
|
|||||||
import {get, post, put} from "./api";
|
import {get, post, put} from "./api";
|
||||||
|
|
||||||
export const fetchUsers = () => get('users');
|
export const fetchUsers: FetchUsers = () => get('users');
|
||||||
|
|
||||||
export const fetchUser = (id: number) => get(`users/${id}`);
|
export const fetchUser: FetchUser = (id: number) => get(`users/${id}`);
|
||||||
export const updateUser = (user: UserType) => put(`users/${user.id}`, { user })
|
export const updateUser: UpdateUser = (user: UserType) => put(`users/${user.id}`, { user });
|
||||||
|
|
||||||
export const fetchCurrentUser = () => get('users/me');
|
export const fetchCurrentUser: FetchCurrentUser = () => get('users/me');
|
||||||
export const fetchUserNovels = (id: number) => get(`users/${id}/novels`);
|
export const fetchUserNovels: FetchUserNovels = (id: number) => get(`users/${id}/novels`);
|
||||||
|
|
||||||
export const activateAccount = (token: string) => post('auth/activate', { token });
|
export const activateAccount: ActivateAccount = (token: string) => post('auth/activate', { token });
|
||||||
export const generateActivateLink = () => post('auth/activateToken');
|
export const generateActivateLink: GenerateActivateLink = () => post('auth/activateToken');
|
||||||
|
|
||||||
export const fetchUserSettings = () => get('users/me/settings');
|
export const fetchUserSettings: FetchUserSettings = () => get('users/me/settings');
|
||||||
export const updateUserSettings = (use_gravatar: boolean, avatar: string) => post('users/me/settings', { use_gravatar, avatar });
|
export const updateUserGeneralSettings: UpdateUserGeneralSettings = (use_gravatar: boolean, avatar: string) => post('users/me/settings', { use_gravatar, avatar });
|
||||||
|
export const updateUserAppearanceSettings: UpdateUserAppearanceSettings = (theme: string) => post('users/me/settings', { theme });
|
||||||
export const uploadAvatar = (avatar: FormData) => post('users/me/avatar', 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 });
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class AdminPanelUsers extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
fetchUsers().then(users => this.setState({ users }))
|
fetchUsers().then(users => this.setState({ users }));
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -46,4 +46,4 @@ export default connect(
|
|||||||
(state: StoreState) => ({
|
(state: StoreState) => ({
|
||||||
user: state.currentUser
|
user: state.currentUser
|
||||||
})
|
})
|
||||||
)(AdminPanelUsers);
|
)(AdminPanelUsers);
|
||||||
|
|||||||
@@ -41,11 +41,19 @@ class App extends React.Component<Props, State> {
|
|||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const cachedTheme = localStorage.getItem('theme');
|
||||||
|
if(cachedTheme)
|
||||||
|
document.body.setAttribute('theme', cachedTheme);
|
||||||
|
|
||||||
const { setUser } = this.props;
|
const { setUser } = this.props;
|
||||||
fetchCurrentUser().then((user: UserType) => {
|
fetchCurrentUser().then((user: UserType) => {
|
||||||
setUser(user);
|
setUser(user);
|
||||||
fetchUserSettings().then((settings: UserSettingsType) => {
|
fetchUserSettings().then((settings: UserSettingsType) => {
|
||||||
document.body.setAttribute('theme', settings.theme);
|
console.log(settings.theme, cachedTheme)
|
||||||
|
if(settings.theme !== cachedTheme) {
|
||||||
|
localStorage.setItem('theme', settings.theme);
|
||||||
|
document.body.setAttribute('theme', settings.theme);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}).catch((err: ApiError) => {
|
}).catch((err: ApiError) => {
|
||||||
if(err.code !== 3 && err.text) console.error(err.text);
|
if(err.code !== 3 && err.text) console.error(err.text);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@import '../../Constants'
|
@import '../../Constants'
|
||||||
|
|
||||||
.Comments
|
.Comments
|
||||||
margin-top: 1rem
|
margin-top: 1rem
|
||||||
padding: $primaryPadding
|
padding: $primaryPadding
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
.Comments_TextArea
|
.Comments_TextArea
|
||||||
width: 100%
|
width: 100%
|
||||||
|
color: var(--main-color)
|
||||||
background-color: var(--block-color)
|
background-color: var(--block-color)
|
||||||
font-size: 1rem
|
font-size: 1rem
|
||||||
padding: .25rem
|
padding: .25rem
|
||||||
@@ -43,4 +44,4 @@
|
|||||||
border-radius: .2rem
|
border-radius: .2rem
|
||||||
border: none
|
border: none
|
||||||
font-size: 1rem
|
font-size: 1rem
|
||||||
padding: .5rem
|
padding: .5rem
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ $hoverColor: #f22
|
|||||||
position: relative
|
position: relative
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
.Header
|
.Navbar
|
||||||
color: var(--main-color)
|
color: var(--main-color)
|
||||||
background-color: var(--nav-bg-color)
|
background-color: var(--nav-bg-color)
|
||||||
display: flex
|
display: flex
|
||||||
@@ -52,6 +52,21 @@ $hoverColor: #f22
|
|||||||
&::before
|
&::before
|
||||||
height: 5px
|
height: 5px
|
||||||
|
|
||||||
|
&__Dropdown
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
&:hover>&_Menu
|
||||||
|
display: block
|
||||||
|
|
||||||
|
&_Menu
|
||||||
|
display: none
|
||||||
|
position: absolute
|
||||||
|
background-color: #fefefe
|
||||||
|
box-sizing: border-box
|
||||||
|
padding: .5rem
|
||||||
|
|
||||||
|
&_Item
|
||||||
|
margin: .2rem 0
|
||||||
&__User
|
&__User
|
||||||
justify-content: flex-end
|
justify-content: flex-end
|
||||||
|
|
||||||
@@ -90,4 +105,4 @@ $hoverColor: #f22
|
|||||||
width: 40px
|
width: 40px
|
||||||
height: 40px
|
height: 40px
|
||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
margin-right: .2rem
|
margin-right: .2rem
|
||||||
|
|||||||
+16
-19
@@ -11,7 +11,7 @@ import RegisterModal from "../Modals/RegisterModal";
|
|||||||
import LoginModal from "../Modals/LoginModal";
|
import LoginModal from "../Modals/LoginModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setModal: SetModalType
|
setModal: SetModal
|
||||||
logout: () => void
|
logout: () => void
|
||||||
currentUser: UserType
|
currentUser: UserType
|
||||||
};
|
};
|
||||||
@@ -50,48 +50,45 @@ class Navbar extends React.Component<Props, State> {
|
|||||||
const isMinimized = window.screen.width <= 1000;
|
const isMinimized = window.screen.width <= 1000;
|
||||||
|
|
||||||
const links = (
|
const links = (
|
||||||
<div className="Header__Links">
|
<div className="Navbar__Links">
|
||||||
<Link to="/" className={'Header_Button'}>
|
<Link to="/" className={'Navbar_Button'}>
|
||||||
Главная
|
Главная
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/novels" className={'Header_Button'}>
|
<Link to="/novels" className={'Navbar_Button'}>
|
||||||
Новеллы
|
Новеллы
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/users" className={'Header_Button'}>
|
|
||||||
Пользователи
|
|
||||||
</Link>
|
|
||||||
{expand && isMinimized && (
|
{expand && isMinimized && (
|
||||||
<div onClick={this.changeSize} className={'Header_Button'} style={{ position: "absolute", right: 0, margin: '.5rem' }}>X</div>
|
<div onClick={this.changeSize} className={'Navbar_Button'} style={{ position: "absolute", right: 0, margin: '.5rem' }}>X</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={`Header ${expand ? 'Expanded' : 'Minimized'}`}>
|
<nav className={`Navbar ${expand ? 'Expanded' : 'Minimized'}`}>
|
||||||
{expand || !isMinimized ? links : (
|
{expand || !isMinimized ? links : (
|
||||||
<div className="Header__Links">
|
<div className="Navbar__Links">
|
||||||
<div onClick={this.changeSize} className={'Header_Button'}>
|
<div onClick={this.changeSize} className={'Navbar_Button'}>
|
||||||
<FontAwesomeIcon icon={faBars}/>
|
<FontAwesomeIcon icon={faBars}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{currentUser &&
|
{currentUser &&
|
||||||
(<div className={'Header__User'}>
|
(<div className={'Navbar__User'}>
|
||||||
{currentUser.authorized ? (<>
|
{currentUser.authorized ? (<>
|
||||||
<Link to={`/user/${currentUser.id}`} className="Header__User_Profile">
|
<Link to={`/user/${currentUser.id}`} className="Navbar__User_Profile">
|
||||||
<div className={'Header__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar}?s=40&t=${new Date().getTime()})` }}/>
|
<div className={'Navbar__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={'Navbar_Button'}>
|
||||||
Админ-панель
|
Админ-панель
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link className="Header_Button" to={`/user/${currentUser.id}/settings`}>Настройки</Link>
|
<Link className="Navbar_Button" to={`/user/${currentUser.id}/settings`}>Настройки</Link>
|
||||||
<div className={'Header_Button'} onClick={this.logout}>Выйти</div>
|
<div className={'Navbar_Button'} onClick={this.logout}>Выйти</div>
|
||||||
</>) : (<>
|
</>) : (<>
|
||||||
<div className={'Header_Button'} onClick={() => setModal(<RegisterModal/>)}>Регистрация</div>
|
<div className={'Navbar_Button'} onClick={() => setModal(<RegisterModal/>)}>Регистрация</div>
|
||||||
<div className={'Header_Button'} onClick={() => setModal(<LoginModal/>)}>Войти</div>
|
<div className={'Navbar_Button'} onClick={() => setModal(<LoginModal/>)}>Войти</div>
|
||||||
</>)}
|
</>)}
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,45 +93,6 @@
|
|||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
|
|
||||||
form
|
|
||||||
width: 100%
|
|
||||||
margin-bottom: .5rem
|
|
||||||
|
|
||||||
.Novel__Comments_BBCodes
|
|
||||||
font-size: 1.3rem
|
|
||||||
margin-bottom: .2rem
|
|
||||||
|
|
||||||
svg
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
svg:first-child
|
|
||||||
padding-right: .5rem
|
|
||||||
|
|
||||||
svg:last-child
|
|
||||||
padding-left: .5rem
|
|
||||||
|
|
||||||
svg:not(:first-child):not(:last-child)
|
|
||||||
padding: 0 .5rem
|
|
||||||
|
|
||||||
.Novel__Comments_TextArea
|
|
||||||
width: 100%
|
|
||||||
background-color: var(--block-color)
|
|
||||||
font-size: 1rem
|
|
||||||
padding: .25rem
|
|
||||||
border: none
|
|
||||||
border-radius: $borderRadius
|
|
||||||
box-sizing: border-box
|
|
||||||
min-height: 4rem
|
|
||||||
resize: none
|
|
||||||
margin-bottom: .5rem
|
|
||||||
|
|
||||||
.Novel__Comments_Submit
|
|
||||||
background-color: var(--block-color) !important
|
|
||||||
border-radius: $borderRadius
|
|
||||||
border: none
|
|
||||||
font-size: 1rem
|
|
||||||
padding: .5rem
|
|
||||||
|
|
||||||
.Novel__Characters
|
.Novel__Characters
|
||||||
margin-top: 1rem
|
margin-top: 1rem
|
||||||
padding: $primaryPadding 0
|
padding: $primaryPadding 0
|
||||||
@@ -169,4 +130,4 @@
|
|||||||
height: calc(100% - 1.5rem)
|
height: calc(100% - 1.5rem)
|
||||||
background-size: cover
|
background-size: cover
|
||||||
background-position: center
|
background-position: center
|
||||||
margin-bottom: .2rem
|
margin-bottom: .2rem
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class UserNovels extends React.Component<Props, State> {
|
|||||||
|
|
||||||
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 }));
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ class UserNovels extends React.Component<Props, State> {
|
|||||||
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/>;
|
||||||
@@ -92,4 +93,4 @@ class UserNovels extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserNovels;
|
export default UserNovels;
|
||||||
|
|||||||
@@ -4,8 +4,20 @@ import {connect} from "react-redux";
|
|||||||
import NotFoundError from "../../NotFoundError";
|
import NotFoundError from "../../NotFoundError";
|
||||||
import Button from "../../../ui/Button/Button";
|
import Button from "../../../ui/Button/Button";
|
||||||
import './Settings.sass';
|
import './Settings.sass';
|
||||||
import {fetchUserSettings, updateUserSettings, uploadAvatar} from "../../../api/users";
|
import {
|
||||||
|
fetchUserSettings,
|
||||||
|
updateUserAppearanceSettings,
|
||||||
|
updateUserGeneralSettings,
|
||||||
|
uploadAvatar
|
||||||
|
} from "../../../api/users";
|
||||||
import {setUser} from "../../../store/actions";
|
import {setUser} from "../../../store/actions";
|
||||||
|
import Tabs from "../../../ui/Tabs/Tabs";
|
||||||
|
import TabItem from "../../../ui/Tabs/TabItem";
|
||||||
|
|
||||||
|
enum TabsNames {
|
||||||
|
General,
|
||||||
|
Appearance
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { userId: string } }
|
match: { params: { userId: string } }
|
||||||
@@ -17,11 +29,15 @@ type State = {
|
|||||||
// Avatar
|
// Avatar
|
||||||
useGravatar: boolean
|
useGravatar: boolean
|
||||||
avatar: File | null
|
avatar: File | null
|
||||||
|
theme: Theme
|
||||||
|
|
||||||
|
currentTab: TabsNames
|
||||||
}
|
}
|
||||||
|
|
||||||
class Settings extends React.Component<Props, State> {
|
class Settings extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
useGravatar: false, avatar: null
|
useGravatar: false, avatar: null, theme: "dark",
|
||||||
|
currentTab: TabsNames.General
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -38,7 +54,7 @@ class Settings extends React.Component<Props, State> {
|
|||||||
|
|
||||||
saveAvatar = (event: FormEvent) => {
|
saveAvatar = (event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const { avatar, useGravatar } = this.state;
|
const { avatar, useGravatar} = this.state;
|
||||||
const { setUser } = this.props;
|
const { setUser } = this.props;
|
||||||
|
|
||||||
if(avatar) {
|
if(avatar) {
|
||||||
@@ -46,28 +62,71 @@ class Settings extends React.Component<Props, State> {
|
|||||||
formData.append('avatar', avatar);
|
formData.append('avatar', avatar);
|
||||||
|
|
||||||
uploadAvatar(formData).then(avatarUrl => {
|
uploadAvatar(formData).then(avatarUrl => {
|
||||||
updateUserSettings(useGravatar, avatarUrl).then(setUser)
|
updateUserGeneralSettings(useGravatar, avatarUrl).then(setUser)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
updateUserSettings(useGravatar, '').then(setUser)
|
updateUserGeneralSettings(useGravatar, '').then(setUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveTheme = (event: FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { theme } = this.state;
|
||||||
|
updateUserAppearanceSettings(theme).then(user => {
|
||||||
|
setUser(user);
|
||||||
|
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
document.body.setAttribute('theme', theme);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentUser, match: { params: { userId } } } = this.props;
|
const { currentUser, match: { params: { userId } } } = this.props;
|
||||||
const { useGravatar } = this.state
|
const { useGravatar, currentTab, theme } = this.state
|
||||||
|
|
||||||
if(parseInt(userId) !== currentUser.id) return <NotFoundError/>;
|
if(parseInt(userId) !== currentUser.id) return <NotFoundError/>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group title={'Настройки'}>
|
<Group title={'Настройки'}>
|
||||||
<Group title={'Аватар'} className={'Settings_Avatar'}>
|
<Tabs>
|
||||||
<form onSubmit={this.saveAvatar}>
|
<TabItem
|
||||||
<label>Использовать Gravatar <input type="checkbox" onChange={event => this.setState({ useGravatar: event.target.checked })} checked={useGravatar}/></label>
|
selected={currentTab === TabsNames.General}
|
||||||
<label>Загрузить аватар <input type="file" onChange={this.onChangeAvatar} disabled={useGravatar}/></label>
|
onClick={() => this.setState({ currentTab: TabsNames.General })}
|
||||||
<Button>Сохранить</Button>
|
>
|
||||||
</form>
|
Главное
|
||||||
</Group>
|
</TabItem>
|
||||||
|
<TabItem
|
||||||
|
selected={currentTab === TabsNames.Appearance}
|
||||||
|
onClick={() => this.setState({ currentTab: TabsNames.Appearance })}
|
||||||
|
>
|
||||||
|
Внешний вид
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
{currentTab === TabsNames.General && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
{currentTab === TabsNames.Appearance && (
|
||||||
|
<Group title={'Тема'} className={'Settings_Avatar'}>
|
||||||
|
<form onSubmit={this.saveTheme}>
|
||||||
|
<select onChange={e => this.setState({ theme: e.target.value as Theme })} value={theme}>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
<option value="blue">Blue</option>
|
||||||
|
<option value="red">Red</option>
|
||||||
|
<option value="green">Green</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<Button>Сохранить</Button>
|
||||||
|
</form>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -80,4 +139,4 @@ export default connect(
|
|||||||
dispatch => ({
|
dispatch => ({
|
||||||
setUser: (user: UserType) => dispatch(setUser(user))
|
setUser: (user: UserType) => dispatch(setUser(user))
|
||||||
})
|
})
|
||||||
)(Settings);
|
)(Settings);
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
background-position: center
|
background-position: center
|
||||||
background-size: cover
|
background-size: cover
|
||||||
background-repeat: no-repeat
|
background-repeat: no-repeat
|
||||||
border-radius: $borderRadius $borderRadius 0 0
|
|
||||||
|
|
||||||
@media (max-width: 500px)
|
@media (max-width: 500px)
|
||||||
height: 10rem !important
|
height: 10rem !important
|
||||||
@@ -34,7 +33,7 @@
|
|||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
width: 100%
|
width: 100%
|
||||||
padding: 0.5rem
|
padding: 0.5rem
|
||||||
border-radius: 0 0 $borderRadius $borderRadius
|
border-radius: $borderRadius
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
|
|
||||||
&_Nick
|
&_Nick
|
||||||
@@ -42,6 +41,7 @@
|
|||||||
font-weight: 600
|
font-weight: 600
|
||||||
|
|
||||||
&_Group
|
&_Group
|
||||||
|
background-color: #0008
|
||||||
border-color: #eeeeee
|
border-color: #eeeeee
|
||||||
border-width: 1px 1px 1px 4px
|
border-width: 1px 1px 1px 4px
|
||||||
border-style: solid
|
border-style: solid
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
border-radius: 4px
|
border-radius: 4px
|
||||||
padding: .2rem
|
padding: .2rem
|
||||||
text-align: center
|
text-align: center
|
||||||
font-size: 1rem
|
font-size: 1.1rem
|
||||||
font-weight: 300
|
font-weight: 300
|
||||||
|
|
||||||
&_Avatar
|
&_Avatar
|
||||||
@@ -142,4 +142,4 @@
|
|||||||
&_Items
|
&_Items
|
||||||
display: flex
|
display: flex
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
justify-content: center
|
justify-content: center
|
||||||
|
|||||||
+34
-5
@@ -7,10 +7,13 @@ import Loading from "../../ui/Loading";
|
|||||||
import NovelItem from "../../ui/NovelItem/NovelItem";
|
import NovelItem from "../../ui/NovelItem/NovelItem";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import Button from "../../ui/Button/Button";
|
import Button from "../../ui/Button/Button";
|
||||||
|
import {addNotification} from "../../store/actions";
|
||||||
|
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { userId: string } }
|
match: { params: { userId: string } }
|
||||||
currentUser: UserType
|
currentUser: UserType
|
||||||
|
addNotification: AddNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -38,7 +41,22 @@ class User extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendActivateLink = () => {
|
sendActivateLink = () => {
|
||||||
generateActivateLink().then(console.log).catch(console.error)
|
generateActivateLink().then(() => {
|
||||||
|
const notification = (
|
||||||
|
<NotificationMessage level={"success"}>
|
||||||
|
Сообщение успешно отправлено!
|
||||||
|
</NotificationMessage>
|
||||||
|
);
|
||||||
|
this.props.addNotification(notification);
|
||||||
|
}).catch(err => {
|
||||||
|
const notification = (
|
||||||
|
<NotificationMessage level={"success"}>
|
||||||
|
Сообщение успешно отправлено!
|
||||||
|
</NotificationMessage>
|
||||||
|
);
|
||||||
|
this.props.addNotification(notification);
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -62,14 +80,22 @@ 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_Main'}>
|
<div className="User__Info_Cover User__Info_Main" style={{ backgroundImage: `url(${user.cover})` }}>
|
||||||
<div className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar}?s=96&t=${new Date().getTime()})` }} />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>) : (
|
||||||
|
<div className={'User__Info_Main'}>
|
||||||
|
<div className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar}?s=96&t=${new Date().getTime()})` }} />
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="User__Statistic">
|
<div className="User__Statistic">
|
||||||
@@ -127,5 +153,8 @@ class User extends React.Component<Props, State> {
|
|||||||
export default connect(
|
export default connect(
|
||||||
(state: StoreState) => ({
|
(state: StoreState) => ({
|
||||||
currentUser: state.currentUser
|
currentUser: state.currentUser
|
||||||
|
}),
|
||||||
|
dispatch => ({
|
||||||
|
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
||||||
})
|
})
|
||||||
)(User);
|
)(User);
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
import {createStore} from "redux";
|
import {createStore} from "redux";
|
||||||
import index from "./reducers";
|
|
||||||
import {composeWithDevTools} from "redux-devtools-extension";
|
import {composeWithDevTools} from "redux-devtools-extension";
|
||||||
|
import reducers from "./reducers/reducers";
|
||||||
|
|
||||||
const composeEnhancers = composeWithDevTools({
|
const composeEnhancers = composeWithDevTools({
|
||||||
trace: true
|
trace: true
|
||||||
});
|
});
|
||||||
const store = process.env.NODE_ENV === 'development' ? createStore(index, composeEnhancers()) : createStore(index);
|
const store = process.env.NODE_ENV === 'development' ? createStore(reducers, composeEnhancers()) : createStore(reducers);
|
||||||
export default store;
|
export default store;
|
||||||
|
|||||||
Vendored
+29
@@ -0,0 +1,29 @@
|
|||||||
|
type ApiRequest = (url: string, method: string, body?: string | FormData) => Promise<any>;
|
||||||
|
|
||||||
|
|
||||||
|
type Get = (url: string, data?: object) => Promise<any>;
|
||||||
|
type Post = (url: string, data?: object | FormData) => Promise<any>;
|
||||||
|
type Put = (url: string, data?: object) => Promise<any>;
|
||||||
|
type Delete = (url: string, data?: object) => Promise<any>;
|
||||||
|
|
||||||
|
|
||||||
|
type FetchUsers = () => Promise<UserType[]>;
|
||||||
|
type FetchUser = (id: number) => Promise<UserType>;
|
||||||
|
type UpdateUser = (user: UserType) => Promise<UserType>;
|
||||||
|
type FetchCurrentUser = () => Promise<UserType>;
|
||||||
|
type FetchUserNovels = (id: number) => Promise<NovelType[]>;
|
||||||
|
type ActivateAccount = (token: string) => Promise<string>;
|
||||||
|
type GenerateActivateLink = () => Promise<string>;
|
||||||
|
type FetchUserSettings = () => Promise<UserSettingsType>;
|
||||||
|
type UpdateUserGeneralSettings = (use_gravatar: boolean, avatar: string) => Promise<UserType>;
|
||||||
|
type UpdateUserAppearanceSettings = (theme: string) => Promise<UserType>;
|
||||||
|
|
||||||
|
|
||||||
|
type FetchChar = (id: number) => Promise<CharacterType>;
|
||||||
|
type FetchCharNovels = (id: number) => Promise<NovelType[]>;
|
||||||
|
|
||||||
|
|
||||||
|
type Login = (login: string, password: string, recaptcha: string) => Promise<UserType>;
|
||||||
|
type Register = (login: string, password: string, email: string, recaptcha: string) => Promise<UserType>;
|
||||||
|
type Restore = (token: string, new_password: string) => Promise<string>;
|
||||||
|
type Logout = () => Promise<string>;
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
type StoreState = {
|
||||||
|
currentUser: UserType
|
||||||
|
notifications: React.ReactNode[]
|
||||||
|
modal: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetUser = (user: UserType) => void
|
||||||
|
type LogoutUser = () => void
|
||||||
|
type SetModal = (modal: React.ReactNode | null) => void
|
||||||
|
type HideModal = () => void
|
||||||
|
type AddNotification = (notification: React.ReactNode) => void
|
||||||
+4
-10
@@ -5,10 +5,12 @@ type ApiError = {
|
|||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DataType = NovelType | NovelType[] | CharacterType | CharacterType[] | UserType | UserType[] | UserSettingsType | UserNovelType | UserNovelType[] | UserGroupType | CommentType | CommentType[] | GenreType | GenreType[] | PostType | PostType[];
|
||||||
|
|
||||||
type ApiResponse = {
|
type ApiResponse = {
|
||||||
error: boolean
|
error: boolean
|
||||||
data?: any
|
error_data?: ApiError | string
|
||||||
error_data?: ApiError
|
data?: DataType
|
||||||
}
|
}
|
||||||
|
|
||||||
type NovelType = {
|
type NovelType = {
|
||||||
@@ -84,11 +86,3 @@ type PostType = {
|
|||||||
author: string
|
author: string
|
||||||
author_id: number
|
author_id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type StoreState = {
|
|
||||||
currentUser: UserType
|
|
||||||
notifications: React.ReactNode[]
|
|
||||||
modal: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetModalType = (modal: React.ReactNode | null) => void
|
|
||||||
@@ -18,4 +18,4 @@
|
|||||||
&__Content
|
&__Content
|
||||||
.Comment__User_Username
|
.Comment__User_Username
|
||||||
font-size: 1.1rem
|
font-size: 1.1rem
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface Props extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||||
|
selected?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabItem = ({ children, selected, ...props }: Props) => (
|
||||||
|
<div className={selected ? 'TabItem Selected' : 'TabItem'} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default TabItem;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
.Tabs
|
||||||
|
display: flex
|
||||||
|
justify-content: space-around
|
||||||
|
|
||||||
|
.TabItem
|
||||||
|
cursor: pointer
|
||||||
|
font-size: 1.2rem
|
||||||
|
color: var(--sec-color)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: var(--link-color)
|
||||||
|
|
||||||
|
&.Selected
|
||||||
|
color: var(--main-color)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import './Tabs.sass';
|
||||||
|
|
||||||
|
const Tabs = ({ children }: { children: React.ReactNode[] }) => (
|
||||||
|
<div className={'Tabs'}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Tabs;
|
||||||
+2
-2
@@ -7,7 +7,6 @@
|
|||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -17,7 +16,8 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react"
|
"jsx": "react",
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
|||||||
Reference in New Issue
Block a user