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

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 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) => { const request: ApiRequest = async (url: string, method: string, body?: string | FormData) => {
let bUrl = baseUrl; let bUrl = baseUrl;
+5 -2
View File
@@ -45,7 +45,7 @@ class App extends React.Component<Props, State> {
if(cachedTheme) if(cachedTheme)
document.body.setAttribute('theme', cachedTheme); document.body.setAttribute('theme', cachedTheme);
const { setUser } = this.props; const { setUser } = props;
fetchCurrentUser().then((user: UserType) => { fetchCurrentUser().then((user: UserType) => {
setUser(user); setUser(user);
fetchUserSettings().then((settings: UserSettingsType) => { fetchUserSettings().then((settings: UserSettingsType) => {
@@ -55,7 +55,8 @@ class App extends React.Component<Props, State> {
} }
}); });
}).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);
}); });
parser.registerTag('spoiler', SpoilerTag); parser.registerTag('spoiler', SpoilerTag);
@@ -67,6 +68,8 @@ class App extends React.Component<Props, State> {
caches.keys().then(names => { caches.keys().then(names => {
names.forEach(name => caches.delete(name).catch(console.error)) 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'; import { faDiscord, faVk, faTelegramPlane } from '@fortawesome/free-brands-svg-icons';
const Footer: React.FC = () => { 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 ( return (
<div className='Footer'> <div className='Footer'>
<footer className='Footer__Tabs'> <footer className='Footer__Tabs'>
<div className="Footer__Tab"> <div className="Footer__Tab">
<div className="Footer__Tab_Title" onClick={secretChangeTheme}>Команда</div> <div className="Footer__Tab_Title">Команда</div>
<Link to={'/user/1'} className={'Link'}>Nix13</Link> <Link to={'/user/1'} className={'Link'}>Nix13</Link>
<Link to={'/user/6'} className={'Link'}>RonFall</Link>
</div> </div>
<div className="Footer__Tab"> <div className="Footer__Tab">
<div className="Footer__Tab_Title">Соцсети</div> <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 {capitalize, generateClassName, getRandomInt} from "../../../utils";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAlignJustify, faThLarge} from "@fortawesome/free-solid-svg-icons"; import {faAlignJustify, faThLarge} from "@fortawesome/free-solid-svg-icons";
import Button from "../../../ui/Button/Button";
type Props = { type Props = {
match: { params: { userId: number } } match: { params: { userId: number } }
@@ -73,7 +74,7 @@ class UserNovels extends React.Component<Props, State> {
{planned.length > 0 && <div className={'UserNovels__List'}> {planned.length > 0 && <div className={'UserNovels__List'}>
<div className="UserNovels__List_Title"> <div className="UserNovels__List_Title">
<div style={{ marginRight: '1rem' }}>Запланированные</div> <div style={{ marginRight: '1rem' }}>Запланированные</div>
<button onClick={this.getRandomNovel}>Рандомная новелла</button> <Button onClick={this.getRandomNovel}>Рандомная новелла</Button>
</div> </div>
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}> <div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
{planned.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id+1} viewType={viewType}/>)} {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 .Group
.Settings_Avatar .Settings_Group
font-size: 1.1rem
form form
display: flex display: flex
flex-direction: column flex-direction: column
@@ -8,5 +10,5 @@
input input
margin: .5rem 0 margin: .5rem 0
.Button input, .Button
flex-grow: 0 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 Group from "../../../ui/Group/Group";
import {connect} from "react-redux"; import {connect} from "react-redux";
import NotFoundError from "../../NotFoundError"; import NotFoundError from "../../NotFoundError";
import Button from "../../../ui/Button/Button";
import './Settings.sass'; import './Settings.sass';
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 Tabs from "../../../ui/Tabs/Tabs";
import TabItem from "../../../ui/Tabs/TabItem"; import TabItem from "../../../ui/Tabs/TabItem";
import Appearance from "./Appearance";
import General from "./General";
import Security from "./Security";
enum TabsNames { enum TabsNames {
General, General,
Security,
Appearance Appearance
} }
@@ -26,69 +23,22 @@ type Props = {
} }
type State = { type State = {
// Avatar
useGravatar: boolean
avatar: File | null
theme: Theme
currentTab: TabsNames currentTab: TabsNames
} }
class Settings extends React.Component<Props, State> { class Settings extends React.Component<Props, State> {
state: State = { state: State = {
useGravatar: false, avatar: null, theme: "dark",
currentTab: TabsNames.General 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() { render() {
const { currentUser, match: { params: { userId } } } = this.props; const { currentUser, match: { params: { userId } } } = this.props;
const { useGravatar, currentTab, theme } = this.state const { currentTab } = this.state
if(parseInt(userId) !== currentUser.id) return <NotFoundError/>; if(parseInt(userId) !== currentUser.id) return <NotFoundError/>;
return ( return (
<Group title={'Настройки'}> <Group title={''}>
<Tabs> <Tabs>
<TabItem <TabItem
selected={currentTab === TabsNames.General} selected={currentTab === TabsNames.General}
@@ -96,6 +46,12 @@ class Settings extends React.Component<Props, State> {
> >
Главное Главное
</TabItem> </TabItem>
<TabItem
selected={currentTab === TabsNames.Security}
onClick={() => this.setState({ currentTab: TabsNames.Security })}
>
Безопастность
</TabItem>
<TabItem <TabItem
selected={currentTab === TabsNames.Appearance} selected={currentTab === TabsNames.Appearance}
onClick={() => this.setState({ currentTab: TabsNames.Appearance })} onClick={() => this.setState({ currentTab: TabsNames.Appearance })}
@@ -104,28 +60,13 @@ class Settings extends React.Component<Props, State> {
</TabItem> </TabItem>
</Tabs> </Tabs>
{currentTab === TabsNames.General && ( {currentTab === TabsNames.General && (
<Group title={'Аватар'} className={'Settings_Avatar'}> <General setUser={setUser}/>
<form onSubmit={this.saveAvatar}> )}
<label>Использовать Gravatar <input type="checkbox" onChange={event => this.setState({ useGravatar: event.target.checked })} checked={useGravatar}/></label> {currentTab === TabsNames.Security && (
<label>Загрузить аватар <input type="file" onChange={this.onChangeAvatar} disabled={useGravatar}/></label> <Security/>
<Button>Сохранить</Button>
</form>
</Group>
)} )}
{currentTab === TabsNames.Appearance && ( {currentTab === TabsNames.Appearance && (
<Group title={'Тема'} className={'Settings_Avatar'}> <Appearance/>
<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>
); );
+22 -8
View File
@@ -27,19 +27,32 @@ class User extends React.Component<Props, State> {
user: null, novels: [], errorCode: null user: null, novels: [], errorCode: null
}; };
componentDidMount() { getAndSetUser = (id: number) => {
const params = this.props.match.params; fetchUser(id).then(user => {
fetchUser(parseInt(params.userId)).then((user: UserType) => {
this.setState({ user }); this.setState({ user });
document.title = `${user.username} / Unmei`; document.title = `${user.username} / Unmei`;
fetchUserNovels(parseInt(params.userId)).then((novels: NovelType[]) => { fetchUserNovels(id).then(novels => {
this.setState({ novels }); this.setState({ novels });
}).catch((err: ApiError) => console.error(err)); }).catch((err: ApiError) => console.error(err));
}).catch((err: ApiError) => this.setState({ errorCode: err.code })); }).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 = () => { sendActivateLink = () => {
generateActivateLink().then(() => { generateActivateLink().then(() => {
const notification = ( const notification = (
@@ -48,10 +61,11 @@ class User extends React.Component<Props, State> {
</NotificationMessage> </NotificationMessage>
); );
this.props.addNotification(notification); this.props.addNotification(notification);
}).catch(err => { }).catch((err: ApiError) => {
const notification = ( const notification = (
<NotificationMessage level={"success"}> <NotificationMessage level={"error"}>
Сообщение успешно отправлено! Произошла ошибка! {err.code} <br/>
{err.text}
</NotificationMessage> </NotificationMessage>
); );
this.props.addNotification(notification); this.props.addNotification(notification);
+3
View File
@@ -9,3 +9,6 @@
font-size: 1.2rem font-size: 1.2rem
font-weight: 600 font-weight: 600
margin-bottom: .5rem margin-bottom: .5rem
&__Content
margin-left: .5rem
+1 -1
View File
@@ -19,7 +19,7 @@ const Group = (restProps: Props) => {
return ( return (
<div className={classNames('Group', className)} {...props}> <div className={classNames('Group', className)} {...props}>
<div className="Group__Title">{title}</div> <div className="Group__Title">{title}</div>
<div style={style}> <div style={style} className={'Group__Content'}>
{children} {children}
</div> </div>
</div> </div>
+1 -1
View File
@@ -2,7 +2,7 @@ import React, {InputHTMLAttributes} from "react";
import classNames from "../../classNames"; import classNames from "../../classNames";
import './Input.sass'; import './Input.sass';
const Input = (restProps: InputHTMLAttributes<HTMLElement>) => { const Input = (restProps: InputHTMLAttributes<HTMLInputElement>) => {
const { className, ...props } = restProps; const { className, ...props } = restProps;
return ( return (
<input className={classNames('Input', className)} {...props} /> <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(" ")}` 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 };