api types; navbar style refactoring; user themes
This commit is contained in:
+2
-1
@@ -43,7 +43,8 @@
|
||||
"not op_mini all"
|
||||
],
|
||||
"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 response = async (url: string, method: string, body?: string | FormData) => {
|
||||
const request: ApiRequest = async (url: string, method: string, body?: string | FormData) => {
|
||||
let bUrl = baseUrl;
|
||||
if(baseUrl.endsWith('/')) bUrl = bUrl.slice(0, baseUrl.length-1);
|
||||
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 => {
|
||||
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(!res.ok) return Promise.reject(res.statusText);
|
||||
return Promise.resolve(json.data);
|
||||
}
|
||||
|
||||
const get = (url: string, data?: object) => response(url, 'GET', JSON.stringify(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', JSON.stringify(data));
|
||||
const del = (url: string, data?: object) => response(url, 'DELETE', JSON.stringify(data));
|
||||
const get: Get = (url: string, data?: object) => request(url, 'GET', JSON.stringify(data));
|
||||
const post: Post = (url: string, data?: object | FormData) => request(url, 'POST', data instanceof FormData ? data : JSON.stringify(data));
|
||||
const put: Put = (url: string, data?: object) => request(url, 'PUT', JSON.stringify(data));
|
||||
const del: Delete = (url: string, data?: object) => request(url, 'DELETE', JSON.stringify(data));
|
||||
|
||||
const TranslateStatus: { [id: string]: string } = {
|
||||
planned: 'Запланировано',
|
||||
@@ -39,6 +39,6 @@ const TranslateStatus: { [id: string]: string } = {
|
||||
};
|
||||
|
||||
export const getVersion = () => get('version');
|
||||
export const version = '0.10';
|
||||
export const version = '0.10b';
|
||||
|
||||
export { get, post, put, del, TranslateStatus };
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
import {post} from "./api";
|
||||
|
||||
export const 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 login = (login: string, password: string, recaptcha: string) => post('auth/login', { login, password, recaptcha });
|
||||
export const userLogout = () => post('auth/logout');
|
||||
export const restore: Restore = (token: string, new_password: string) => post('auth/restore', { token, new_password });
|
||||
export const registerUser: Register = (login: string, password: string, email: string, recaptcha: string) => post('auth/register', { login, password, email, recaptcha });
|
||||
export const login: Login = (login: string, password: string, recaptcha: string) => post('auth/login', { login, password, recaptcha });
|
||||
export const userLogout: Logout = () => post('auth/logout');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {get} from "./api";
|
||||
|
||||
export const fetchChar = (id: number) => get(`characters/${id}`);
|
||||
export const fetchCharNovels = (id: number) => get(`characters/${id}/novels`);
|
||||
export const fetchChar: FetchChar = (id: number) => get(`characters/${id}`);
|
||||
export const fetchCharNovels: FetchCharNovels = (id: number) => get(`characters/${id}/novels`);
|
||||
|
||||
+10
-9
@@ -1,18 +1,19 @@
|
||||
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 updateUser = (user: UserType) => put(`users/${user.id}`, { user })
|
||||
export const fetchUser: FetchUser = (id: number) => get(`users/${id}`);
|
||||
export const updateUser: UpdateUser = (user: UserType) => put(`users/${user.id}`, { user });
|
||||
|
||||
export const fetchCurrentUser = () => get('users/me');
|
||||
export const fetchUserNovels = (id: number) => get(`users/${id}/novels`);
|
||||
export const fetchCurrentUser: FetchCurrentUser = () => get('users/me');
|
||||
export const fetchUserNovels: FetchUserNovels = (id: number) => get(`users/${id}/novels`);
|
||||
|
||||
export const activateAccount = (token: string) => post('auth/activate', { token });
|
||||
export const generateActivateLink = () => post('auth/activateToken');
|
||||
export const activateAccount: ActivateAccount = (token: string) => post('auth/activate', { token });
|
||||
export const generateActivateLink: 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 fetchUserSettings: FetchUserSettings = () => get('users/me/settings');
|
||||
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 generateRestoreLink = (email: string, recaptcha: string) => post('auth/restoreToken', { email, recaptcha });
|
||||
|
||||
@@ -17,7 +17,7 @@ class AdminPanelUsers extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetchUsers().then(users => this.setState({ users }))
|
||||
fetchUsers().then(users => this.setState({ users }));
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -41,11 +41,19 @@ class App extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const cachedTheme = localStorage.getItem('theme');
|
||||
if(cachedTheme)
|
||||
document.body.setAttribute('theme', cachedTheme);
|
||||
|
||||
const { setUser } = this.props;
|
||||
fetchCurrentUser().then((user: UserType) => {
|
||||
setUser(user);
|
||||
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) => {
|
||||
if(err.code !== 3 && err.text) console.error(err.text);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
.Comments_TextArea
|
||||
width: 100%
|
||||
color: var(--main-color)
|
||||
background-color: var(--block-color)
|
||||
font-size: 1rem
|
||||
padding: .25rem
|
||||
|
||||
@@ -10,7 +10,7 @@ $hoverColor: #f22
|
||||
position: relative
|
||||
width: 100%
|
||||
|
||||
.Header
|
||||
.Navbar
|
||||
color: var(--main-color)
|
||||
background-color: var(--nav-bg-color)
|
||||
display: flex
|
||||
@@ -52,6 +52,21 @@ $hoverColor: #f22
|
||||
&::before
|
||||
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
|
||||
justify-content: flex-end
|
||||
|
||||
|
||||
+16
-19
@@ -11,7 +11,7 @@ import RegisterModal from "../Modals/RegisterModal";
|
||||
import LoginModal from "../Modals/LoginModal";
|
||||
|
||||
type Props = {
|
||||
setModal: SetModalType
|
||||
setModal: SetModal
|
||||
logout: () => void
|
||||
currentUser: UserType
|
||||
};
|
||||
@@ -50,48 +50,45 @@ class Navbar extends React.Component<Props, State> {
|
||||
const isMinimized = window.screen.width <= 1000;
|
||||
|
||||
const links = (
|
||||
<div className="Header__Links">
|
||||
<Link to="/" className={'Header_Button'}>
|
||||
<div className="Navbar__Links">
|
||||
<Link to="/" className={'Navbar_Button'}>
|
||||
Главная
|
||||
</Link>
|
||||
<Link to="/novels" className={'Header_Button'}>
|
||||
<Link to="/novels" className={'Navbar_Button'}>
|
||||
Новеллы
|
||||
</Link>
|
||||
<Link to="/users" className={'Header_Button'}>
|
||||
Пользователи
|
||||
</Link>
|
||||
{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>
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className={`Header ${expand ? 'Expanded' : 'Minimized'}`}>
|
||||
<nav className={`Navbar ${expand ? 'Expanded' : 'Minimized'}`}>
|
||||
{expand || !isMinimized ? links : (
|
||||
<div className="Header__Links">
|
||||
<div onClick={this.changeSize} className={'Header_Button'}>
|
||||
<div className="Navbar__Links">
|
||||
<div onClick={this.changeSize} className={'Navbar_Button'}>
|
||||
<FontAwesomeIcon icon={faBars}/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{currentUser &&
|
||||
(<div className={'Header__User'}>
|
||||
(<div className={'Navbar__User'}>
|
||||
{currentUser.authorized ? (<>
|
||||
<Link to={`/user/${currentUser.id}`} className="Header__User_Profile">
|
||||
<div className={'Header__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar}?s=40&t=${new Date().getTime()})` }}/>
|
||||
<Link to={`/user/${currentUser.id}`} className="Navbar__User_Profile">
|
||||
<div className={'Navbar__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar}?s=40&t=${new Date().getTime()})` }}/>
|
||||
<div>{this.props.currentUser.username}</div>
|
||||
</Link>
|
||||
{currentUser.group && hasAccessToAdminPanel(currentUser) && (
|
||||
<Link to={'/kawaii__neko'} className={'Header_Button'}>
|
||||
<Link to={'/kawaii__neko'} className={'Navbar_Button'}>
|
||||
Админ-панель
|
||||
</Link>
|
||||
)}
|
||||
<Link className="Header_Button" to={`/user/${currentUser.id}/settings`}>Настройки</Link>
|
||||
<div className={'Header_Button'} onClick={this.logout}>Выйти</div>
|
||||
<Link className="Navbar_Button" to={`/user/${currentUser.id}/settings`}>Настройки</Link>
|
||||
<div className={'Navbar_Button'} onClick={this.logout}>Выйти</div>
|
||||
</>) : (<>
|
||||
<div className={'Header_Button'} onClick={() => setModal(<RegisterModal/>)}>Регистрация</div>
|
||||
<div className={'Header_Button'} onClick={() => setModal(<LoginModal/>)}>Войти</div>
|
||||
<div className={'Navbar_Button'} onClick={() => setModal(<RegisterModal/>)}>Регистрация</div>
|
||||
<div className={'Navbar_Button'} onClick={() => setModal(<LoginModal/>)}>Войти</div>
|
||||
</>)}
|
||||
</div>)
|
||||
}
|
||||
|
||||
@@ -93,45 +93,6 @@
|
||||
background-color: var(--sec-bg-color)
|
||||
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
|
||||
margin-top: 1rem
|
||||
padding: $primaryPadding 0
|
||||
|
||||
@@ -35,6 +35,7 @@ class UserNovels extends React.Component<Props, State> {
|
||||
|
||||
fetchUserNovels(userId)
|
||||
.then(novels => this.setState({ novels }));
|
||||
|
||||
fetchUser(userId)
|
||||
.then(user => this.setState({ user }));
|
||||
|
||||
|
||||
@@ -4,8 +4,20 @@ 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 {
|
||||
fetchUserSettings,
|
||||
updateUserAppearanceSettings,
|
||||
updateUserGeneralSettings,
|
||||
uploadAvatar
|
||||
} from "../../../api/users";
|
||||
import {setUser} from "../../../store/actions";
|
||||
import Tabs from "../../../ui/Tabs/Tabs";
|
||||
import TabItem from "../../../ui/Tabs/TabItem";
|
||||
|
||||
enum TabsNames {
|
||||
General,
|
||||
Appearance
|
||||
}
|
||||
|
||||
type Props = {
|
||||
match: { params: { userId: string } }
|
||||
@@ -17,11 +29,15 @@ type State = {
|
||||
// Avatar
|
||||
useGravatar: boolean
|
||||
avatar: File | null
|
||||
theme: Theme
|
||||
|
||||
currentTab: TabsNames
|
||||
}
|
||||
|
||||
class Settings extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
useGravatar: false, avatar: null
|
||||
useGravatar: false, avatar: null, theme: "dark",
|
||||
currentTab: TabsNames.General
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -38,7 +54,7 @@ class Settings extends React.Component<Props, State> {
|
||||
|
||||
saveAvatar = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
const { avatar, useGravatar } = this.state;
|
||||
const { avatar, useGravatar} = this.state;
|
||||
const { setUser } = this.props;
|
||||
|
||||
if(avatar) {
|
||||
@@ -46,28 +62,71 @@ class Settings extends React.Component<Props, State> {
|
||||
formData.append('avatar', avatar);
|
||||
|
||||
uploadAvatar(formData).then(avatarUrl => {
|
||||
updateUserSettings(useGravatar, avatarUrl).then(setUser)
|
||||
updateUserGeneralSettings(useGravatar, avatarUrl).then(setUser)
|
||||
})
|
||||
} 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() {
|
||||
const { currentUser, match: { params: { userId } } } = this.props;
|
||||
const { useGravatar } = this.state
|
||||
const { useGravatar, currentTab, theme } = 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>
|
||||
<Tabs>
|
||||
<TabItem
|
||||
selected={currentTab === TabsNames.General}
|
||||
onClick={() => this.setState({ currentTab: TabsNames.General })}
|
||||
>
|
||||
Главное
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
background-position: center
|
||||
background-size: cover
|
||||
background-repeat: no-repeat
|
||||
border-radius: $borderRadius $borderRadius 0 0
|
||||
|
||||
@media (max-width: 500px)
|
||||
height: 10rem !important
|
||||
@@ -34,7 +33,7 @@
|
||||
background-color: var(--sec-bg-color)
|
||||
width: 100%
|
||||
padding: 0.5rem
|
||||
border-radius: 0 0 $borderRadius $borderRadius
|
||||
border-radius: $borderRadius
|
||||
box-sizing: border-box
|
||||
|
||||
&_Nick
|
||||
@@ -42,6 +41,7 @@
|
||||
font-weight: 600
|
||||
|
||||
&_Group
|
||||
background-color: #0008
|
||||
border-color: #eeeeee
|
||||
border-width: 1px 1px 1px 4px
|
||||
border-style: solid
|
||||
@@ -49,7 +49,7 @@
|
||||
border-radius: 4px
|
||||
padding: .2rem
|
||||
text-align: center
|
||||
font-size: 1rem
|
||||
font-size: 1.1rem
|
||||
font-weight: 300
|
||||
|
||||
&_Avatar
|
||||
|
||||
+33
-4
@@ -7,10 +7,13 @@ import Loading from "../../ui/Loading";
|
||||
import NovelItem from "../../ui/NovelItem/NovelItem";
|
||||
import {connect} from "react-redux";
|
||||
import Button from "../../ui/Button/Button";
|
||||
import {addNotification} from "../../store/actions";
|
||||
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
|
||||
|
||||
type Props = {
|
||||
match: { params: { userId: string } }
|
||||
currentUser: UserType
|
||||
addNotification: AddNotification
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -38,7 +41,22 @@ class User extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -62,14 +80,22 @@ class User extends React.Component<Props, State> {
|
||||
)}
|
||||
|
||||
<div className="User__Info" style={style}>
|
||||
{user.cover && <div className="User__Info_Cover" style={{ backgroundImage: `url(${user.cover})` }}/>}
|
||||
<div className={'User__Info_Main'}>
|
||||
{user.cover ? (
|
||||
<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>
|
||||
<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 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 className="User__Statistic">
|
||||
@@ -127,5 +153,8 @@ class User extends React.Component<Props, State> {
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
currentUser: state.currentUser
|
||||
}),
|
||||
dispatch => ({
|
||||
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
||||
})
|
||||
)(User);
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import {createStore} from "redux";
|
||||
import index from "./reducers";
|
||||
import {composeWithDevTools} from "redux-devtools-extension";
|
||||
import reducers from "./reducers/reducers";
|
||||
|
||||
const composeEnhancers = composeWithDevTools({
|
||||
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;
|
||||
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
|
||||
}
|
||||
|
||||
type DataType = NovelType | NovelType[] | CharacterType | CharacterType[] | UserType | UserType[] | UserSettingsType | UserNovelType | UserNovelType[] | UserGroupType | CommentType | CommentType[] | GenreType | GenreType[] | PostType | PostType[];
|
||||
|
||||
type ApiResponse = {
|
||||
error: boolean
|
||||
data?: any
|
||||
error_data?: ApiError
|
||||
error_data?: ApiError | string
|
||||
data?: DataType
|
||||
}
|
||||
|
||||
type NovelType = {
|
||||
@@ -84,11 +86,3 @@ type PostType = {
|
||||
author: string
|
||||
author_id: number
|
||||
}
|
||||
|
||||
type StoreState = {
|
||||
currentUser: UserType
|
||||
notifications: React.ReactNode[]
|
||||
modal: React.ReactNode
|
||||
}
|
||||
|
||||
type SetModalType = (modal: React.ReactNode | null) => void
|
||||
@@ -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"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
@@ -17,7 +16,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
"jsx": "react",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
Reference in New Issue
Block a user