изменений опять просто ёбнешься. когда я успел?

This commit is contained in:
2020-10-11 21:55:34 +03:00
parent e617009158
commit 41e2869fa3
16 changed files with 285 additions and 141 deletions
+1
View File
@@ -1 +1,2 @@
GENERATE_SOURCEMAP=false
CDN_URL=https://cdn.unmei.space/
+1 -1
View File
@@ -1,4 +1,4 @@
const baseUrl = process.env.NODE_ENV === "development" ? 'http://localhost:8080/v1' : 'https://api.unmei.space/v1';
const baseUrl = process.env.NODE_ENV === "development" ? 'http://192.168.0.103:8080/v1' : 'https://api.unmei.space/v1';
const request: ApiRequest = async (url: string, method: string, body?: string | FormData) => {
let bUrl = baseUrl;
+5 -2
View File
@@ -45,7 +45,7 @@ class App extends React.Component<Props, State> {
if(cachedTheme)
document.body.setAttribute('theme', cachedTheme);
const { setUser } = this.props;
const { setUser } = props;
fetchCurrentUser().then((user: UserType) => {
setUser(user);
fetchUserSettings().then((settings: UserSettingsType) => {
@@ -55,7 +55,8 @@ class App extends React.Component<Props, State> {
}
});
}).catch((err: ApiError) => {
if(err.code !== 3 && err.text) console.error(err.text);
if(err.code !== 3 && err.text)
console.error(err.text);
});
parser.registerTag('spoiler', SpoilerTag);
@@ -67,6 +68,8 @@ class App extends React.Component<Props, State> {
caches.keys().then(names => {
names.forEach(name => caches.delete(name).catch(console.error))
}).then(() => {
window.location.reload();
});
}
});
+2 -18
View File
@@ -5,29 +5,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faDiscord, faVk, faTelegramPlane } from '@fortawesome/free-brands-svg-icons';
const Footer: React.FC = () => {
const secretChangeTheme = () => {
const theme = localStorage.getItem('theme');
if(theme == null) {
localStorage.setItem('theme', 'light');
document.body.setAttribute('theme', 'light')
} else if(theme === 'light') {
localStorage.setItem('theme', 'dark');
document.body.setAttribute('theme', 'dark')
} else if(theme === 'dark') {
localStorage.setItem('theme', 'light');
document.body.setAttribute('theme', 'light')
}
if(theme !== 'light') {
console.log('Светлая тема находится в разработке! Не все может корректно отображаться.\nК тому же, эта кнопка секретная. Ты знал, на что шел ;)');
}
}
return (
<div className='Footer'>
<footer className='Footer__Tabs'>
<div className="Footer__Tab">
<div className="Footer__Tab_Title" onClick={secretChangeTheme}>Команда</div>
<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>
+2 -1
View File
@@ -6,6 +6,7 @@ import NovelItem from '../../../ui/NovelItem/NovelItem';
import {capitalize, generateClassName, getRandomInt} from "../../../utils";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAlignJustify, faThLarge} from "@fortawesome/free-solid-svg-icons";
import Button from "../../../ui/Button/Button";
type Props = {
match: { params: { userId: number } }
@@ -73,7 +74,7 @@ class UserNovels extends React.Component<Props, State> {
{planned.length > 0 && <div className={'UserNovels__List'}>
<div className="UserNovels__List_Title">
<div style={{ marginRight: '1rem' }}>Запланированные</div>
<button onClick={this.getRandomNovel}>Рандомная новелла</button>
<Button onClick={this.getRandomNovel}>Рандомная новелла</Button>
</div>
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
{planned.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id+1} viewType={viewType}/>)}
+55
View File
@@ -0,0 +1,55 @@
import Button from "../../../ui/Button/Button";
import Group from "../../../ui/Group/Group";
import React, {FormEvent} from "react";
import {fetchUserSettings, updateUserAppearanceSettings} from "../../../api/users";
import {setUser} from "../../../store/actions";
type State = {
theme: Theme
}
class Appearance extends React.Component<{}, State> {
state: State = {
theme: 'dark'
}
componentDidMount() {
fetchUserSettings().then(settings => {
this.setState({ theme: settings.theme })
})
}
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 { theme } = this.state;
return (
<Group title={'Тема'} className={'Settings_Group'}>
<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>
)
}
}
export default Appearance;
+65
View File
@@ -0,0 +1,65 @@
import React, {ChangeEvent, FormEvent} from "react";
import Button from "../../../ui/Button/Button";
import Group from "../../../ui/Group/Group";
import {fetchUserSettings, updateUserGeneralSettings, uploadAvatar} from "../../../api/users";
type Props = {
setUser: (user: UserType) => void
}
type State = {
// Avatar
useGravatar: boolean
avatar: File | null
}
class General extends React.Component<Props, State> {
state: State = {
useGravatar: false, avatar: null
}
componentDidMount() {
fetchUserSettings().then(settings => {
this.setState({ useGravatar: settings.use_gravatar })
})
}
onChangeAvatar = (event: ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if(!files) return;
this.setState({ avatar: files[0] });
}
saveAvatar = (event: FormEvent) => {
event.preventDefault();
const { avatar, useGravatar} = this.state;
const { setUser } = this.props;
if(avatar) {
const formData = new FormData();
formData.append('avatar', avatar);
uploadAvatar(formData).then(avatarUrl => {
updateUserGeneralSettings(useGravatar, avatarUrl).then(setUser)
})
} else {
updateUserGeneralSettings(useGravatar, '').then(setUser)
}
}
render() {
const { useGravatar } = this.state;
return (
<Group title={'Аватар'} className={'Settings_Group'}>
<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>
);
}
}
export default General;
+67
View File
@@ -0,0 +1,67 @@
import React from "react";
import Group from "../../../ui/Group/Group";
import Input from "../../../ui/Input/Input";
import Button from "../../../ui/Button/Button";
type State = {
oldEmail: string
email1: string
email2: string
error: string
}
class Security extends React.Component<{}, State> {
state: State = {
oldEmail: '', email1: '', email2: '',
error: ''
}
changeEmail = (e: React.FormEvent) => {
e.preventDefault();
const { oldEmail, email1, email2 } = this.state;
let error = '';
if(!oldEmail) error += 'Введите старый E-mail\n';
if(!email1) error += 'Введите новый E-mail\n';
if(!email2) error += 'Введите повтор нового E-mail\n';
if(email1 !== email2) error += 'E-mail не совпадают\n';
if(oldEmail === email1) error += 'Старый и новый E-mail не должны совпадать';
if(error) {
this.setState({ error });
return;
}
}
render() {
const { oldEmail, email1, email2, error } = this.state;
return (
<Group title={'Смена E-mail'} className={'Settings_Group'}>
{error && <pre className={'Error'}>{error}</pre>}
<form onSubmit={this.changeEmail}>
<Input
placeholder={'Старый E-mail'}
type={'email'}
value={oldEmail}
onChange={e => this.setState({ oldEmail: e.target.value })}
/>
<Input
placeholder={'Новый E-mail'}
type={'email'}
value={email1}
onChange={e => this.setState({ email1: e.target.value })}
/>
<Input
placeholder={'Новый E-mail ещё раз'}
type={'email'}
value={email2}
onChange={e => this.setState({ email1: e.target.value })}
/>
<Button>Сохранить</Button>
</form>
</Group>
);
}
}
export default Security;
+5 -3
View File
@@ -1,5 +1,7 @@
.Group
.Settings_Avatar
.Settings_Group
font-size: 1.1rem
form
display: flex
flex-direction: column
@@ -8,5 +10,5 @@
input
margin: .5rem 0
.Button
flex-grow: 0
input, .Button
font-size: 1.1rem
+18 -77
View File
@@ -1,21 +1,18 @@
import React, {ChangeEvent, FormEvent} from "react";
import React from "react";
import Group from "../../../ui/Group/Group";
import {connect} from "react-redux";
import NotFoundError from "../../NotFoundError";
import Button from "../../../ui/Button/Button";
import './Settings.sass';
import {
fetchUserSettings,
updateUserAppearanceSettings,
updateUserGeneralSettings,
uploadAvatar
} from "../../../api/users";
import {setUser} from "../../../store/actions";
import Tabs from "../../../ui/Tabs/Tabs";
import TabItem from "../../../ui/Tabs/TabItem";
import Appearance from "./Appearance";
import General from "./General";
import Security from "./Security";
enum TabsNames {
General,
Security,
Appearance
}
@@ -26,69 +23,22 @@ type Props = {
}
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, theme: "dark",
currentTab: TabsNames.General
}
componentDidMount() {
fetchUserSettings().then(settings => {
this.setState({ useGravatar: settings.use_gravatar })
})
}
onChangeAvatar = (event: ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if(!files) return;
this.setState({ avatar: files[0] });
}
saveAvatar = (event: FormEvent) => {
event.preventDefault();
const { avatar, useGravatar} = this.state;
const { setUser } = this.props;
if(avatar) {
const formData = new FormData();
formData.append('avatar', avatar);
uploadAvatar(formData).then(avatarUrl => {
updateUserGeneralSettings(useGravatar, avatarUrl).then(setUser)
})
} else {
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, currentTab, theme } = this.state
const { currentTab } = this.state
if(parseInt(userId) !== currentUser.id) return <NotFoundError/>;
return (
<Group title={'Настройки'}>
<Group title={''}>
<Tabs>
<TabItem
selected={currentTab === TabsNames.General}
@@ -96,6 +46,12 @@ class Settings extends React.Component<Props, State> {
>
Главное
</TabItem>
<TabItem
selected={currentTab === TabsNames.Security}
onClick={() => this.setState({ currentTab: TabsNames.Security })}
>
Безопастность
</TabItem>
<TabItem
selected={currentTab === TabsNames.Appearance}
onClick={() => this.setState({ currentTab: TabsNames.Appearance })}
@@ -104,28 +60,13 @@ class Settings extends React.Component<Props, State> {
</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>
<General setUser={setUser}/>
)}
{currentTab === TabsNames.Security && (
<Security/>
)}
{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>
<Appearance/>
)}
</Group>
);
+22 -8
View File
@@ -27,19 +27,32 @@ class User extends React.Component<Props, State> {
user: null, novels: [], errorCode: null
};
componentDidMount() {
const params = this.props.match.params;
fetchUser(parseInt(params.userId)).then((user: UserType) => {
getAndSetUser = (id: number) => {
fetchUser(id).then(user => {
this.setState({ user });
document.title = `${user.username} / Unmei`;
fetchUserNovels(parseInt(params.userId)).then((novels: NovelType[]) => {
fetchUserNovels(id).then(novels => {
this.setState({ novels });
}).catch((err: ApiError) => console.error(err));
}).catch((err: ApiError) => this.setState({ errorCode: err.code }));
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>, _?: any) {
const newUserId = parseInt(this.props.match.params.userId);
const prevUserId = parseInt(prevProps.match.params.userId);
if(newUserId !== prevUserId) {
this.getAndSetUser(newUserId);
}
}
componentDidMount() {
const params = this.props.match.params;
this.getAndSetUser(parseInt(params.userId))
}
sendActivateLink = () => {
generateActivateLink().then(() => {
const notification = (
@@ -48,10 +61,11 @@ class User extends React.Component<Props, State> {
</NotificationMessage>
);
this.props.addNotification(notification);
}).catch(err => {
}).catch((err: ApiError) => {
const notification = (
<NotificationMessage level={"success"}>
Сообщение успешно отправлено!
<NotificationMessage level={"error"}>
Произошла ошибка! {err.code} <br/>
{err.text}
</NotificationMessage>
);
this.props.addNotification(notification);
+3
View File
@@ -9,3 +9,6 @@
font-size: 1.2rem
font-weight: 600
margin-bottom: .5rem
&__Content
margin-left: .5rem
+1 -1
View File
@@ -19,7 +19,7 @@ const Group = (restProps: Props) => {
return (
<div className={classNames('Group', className)} {...props}>
<div className="Group__Title">{title}</div>
<div style={style}>
<div style={style} className={'Group__Content'}>
{children}
</div>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@ import React, {InputHTMLAttributes} from "react";
import classNames from "../../classNames";
import './Input.sass';
const Input = (restProps: InputHTMLAttributes<HTMLElement>) => {
const Input = (restProps: InputHTMLAttributes<HTMLInputElement>) => {
const { className, ...props } = restProps;
return (
<input className={classNames('Input', className)} {...props} />
+9 -1
View File
@@ -20,4 +20,12 @@ const generateClassName = (baseClassName: string, ...classNames: string[]) => {
else return `${baseClassName} ${classNames.join(" ")}`
}
export { capitalize, getRandomInt, hasPermission, hasAccessToAdminPanel, generateClassName };
const getImage: (path: string) => string = (path: string) => {
let cdnUrl = process.env['CDN_URL'];
if(!cdnUrl) return path;
if(cdnUrl.endsWith('/')) cdnUrl = cdnUrl.slice(cdnUrl.length-1);
if(path.startsWith('/')) path = path.slice(1, path.length);
return `${cdnUrl}/${path}`;
}
export { capitalize, getRandomInt, hasPermission, hasAccessToAdminPanel, generateClassName, getImage };