v1.1; some profile beauty; snowfall

This commit is contained in:
2020-12-22 01:01:47 +03:00
parent 3b54084212
commit a8f62159c7
19 changed files with 710 additions and 631 deletions
+4 -3
View File
@@ -6,8 +6,8 @@
"@fortawesome/fontawesome-svg-core": "1.2.32",
"@fortawesome/free-brands-svg-icons": "5.15.1",
"@fortawesome/free-solid-svg-icons": "5.15.1",
"@fortawesome/react-fontawesome": "0.1.12",
"@sentry/react": "5.27.4",
"@fortawesome/react-fontawesome": "0.1.13",
"@sentry/react": "5.28.0",
"bbcode-to-react": "0.2.9",
"eruda": "2.4.1",
"eruda-dom": "2.0.0",
@@ -18,6 +18,7 @@
"react-loader-spinner": "3.1.14",
"react-redux": "7.2.2",
"react-router-dom": "5.2.0",
"react-snowfall": "^1.0.2",
"redux": "4.0.5"
},
"devDependencies": {
@@ -27,9 +28,9 @@
"@types/react-redux": "7.1.11",
"@types/react-router-dom": "5.1.6",
"@types/react-transition-group": "4.4.0",
"sass": "1.29.0",
"react-scripts": "3.4.4",
"redux-devtools-extension": "2.13.8",
"sass": "1.29.0",
"typescript": "4.1.2"
},
"scripts": {
+12 -15
View File
@@ -2,12 +2,17 @@ const baseUrl = process.env.NODE_ENV === "development" ? 'http://localhost:8080/
async function request<T>(url: string, method: string, body?: string | FormData): Promise<T> {
let bUrl = baseUrl;
if(baseUrl.endsWith('/')) bUrl = bUrl.slice(0, baseUrl.length - 1);
if(url.startsWith('/')) url = url.slice(1, baseUrl.length);
if(baseUrl.endsWith('/'))
bUrl = bUrl.slice(0, baseUrl.length - 1);
if(url.startsWith('/'))
url = url.slice(1, baseUrl.length);
const res = await fetch(`${bUrl}/${url}`, {
method, body,
credentials: "include"
credentials: "include",
headers: new Headers({
'Dev': /dev.(.+)/.test(window.location.href) ? '1' : '0'
})
});
let json: ApiResponse<T>;
@@ -26,18 +31,10 @@ async function request<T>(url: string, method: string, body?: string | FormData)
return Promise.resolve(json.data);
}
function get<T>(url: string, data?: object): Promise<T> {
return request<T>(url, 'GET', JSON.stringify(data));
}
function post<T>(url: string, data?: object | FormData): Promise<T> {
return request(url, 'POST', data instanceof FormData ? data : JSON.stringify(data));
}
function put<T>(url: string, data?: object): Promise<T> {
return request<T>(url, 'PUT', JSON.stringify(data));
}
function del<T>(url: string, data?: object): Promise<T> {
return request<T>(url, 'DELETE', JSON.stringify(data));
}
const get = <T>(url: string, data?: object): Promise<T> => request<T>(url, 'GET', JSON.stringify(data));
const post = <T>(url: string, data?: object | FormData): Promise<T> => request(url, 'POST', data instanceof FormData ? data : JSON.stringify(data));
const put = <T>(url: string, data?: object): Promise<T> => request<T>(url, 'PUT', JSON.stringify(data));
const del = <T>(url: string, data?: object): Promise<T> => request<T>(url, 'DELETE', JSON.stringify(data));
const TranslateStatus: { [id: string]: string } = {
planned: 'Запланировано',
+3
View File
@@ -0,0 +1,3 @@
import { get } from "./api";
export const getClub = (id: number) => get<ClubType>(`/clubs/${id}`);
+13 -2
View File
@@ -65,8 +65,17 @@ body[theme='light']
--block-sec-color: #{$blockSecondaryBackgroundColorLight}
--nav-bg-color: #{$navbarBackgroundColorLight}
color: var(--main-color)
background: var(--main-bg-color)
body[theme='win95']
--main-color: #{$mainColorLight}
--link-color: #{$linkHoverColorLight}
--sec-color: #{$secondaryColorLight}
--main-bg-color: #008083
--sec-bg-color: #c0c0c0
--nav-bg-color: #c0c0c0
.Navbar
box-shadow: 0 1px 1px 1px white
a
color: var(--main-color)
@@ -96,7 +105,9 @@ a
margin-bottom: 1rem
.Error
color: #{mainColor}
background-color: var(--error-bg-color)
.Successful
color: #{mainColor}
background-color: var(--success-bg-color)
+33 -7
View File
@@ -9,8 +9,6 @@ import NotFoundError from "../NotFoundError";
import { fetchCurrentUser, fetchUserSettings } from "../../api/users";
import { connect } from "react-redux";
import Character from "../Character/Character";
// @ts-ignore
import parser from 'bbcode-to-react';
import SpoilerTag from "../../ui/Spoiler/SpoilerTag";
import Novels from "../Novels/Novels";
import Footer from '../Footer/Footer';
@@ -28,21 +26,30 @@ import Settings from "../User/Settings/Settings";
import { setUser } from "../../store/ducks/currentUser";
import { setSettings } from "../../store/ducks/userSettings";
import { withProfiler } from "@sentry/react";
import { hasPermission } from "../../utils";
import Club from "../Club/Club";
import Snowfall from "react-snowfall";
// @ts-ignore
import eruda from 'eruda';
import { hasPermission } from "../../utils";
// @ts-ignore
import parser from 'bbcode-to-react';
type Props = {
notifications: React.ReactNode[]
modal: React.ReactNode
setUser: SetUser
setSettings: SetSettings
snowfall: Snowfall
};
type State = {}
type State = {
user: UserType | null
};
class App extends React.Component<Props, State> {
state: State = {}
state: State = {
user: null
};
constructor(props: Props) {
super(props);
@@ -60,6 +67,9 @@ class App extends React.Component<Props, State> {
fetchCurrentUser().then(user => {
setUser(user);
this.setState({ user });
localStorage.setItem('banned', JSON.stringify(user.is_banned));
fetchUserSettings().then(settings => {
setSettings(settings);
if(settings.theme !== cachedTheme) {
@@ -70,6 +80,7 @@ class App extends React.Component<Props, State> {
if(hasPermission(user, 'mobile_debug'))
eruda.init();
}).catch((err: ApiError) => {
console.log(err)
if(err.code !== 3 && err.text)
console.error(err.text);
});
@@ -88,10 +99,22 @@ class App extends React.Component<Props, State> {
}
render() {
const { modal } = this.props;
const { modal, snowfall } = this.props;
const { user } = this.state;
const isBanned = user?.is_banned || JSON.parse(localStorage.getItem('banned') || 'false');
if(isBanned) {
return (
<div style={{ display: 'flex', height: '100%', justifyContent: "center", alignItems: "center", fontSize: '2rem' }}>
Вы были забанены!
</div>
);
}
return (
<>
{snowfall.snowfallStatus && (
<Snowfall color={'white'} snowflakeCount={snowfall.snowflakeCount} style={{ zIndex: 1000 }}/>
)}
<Navbar/>
<div className={'Container'}>
<div className="Notifications">
@@ -110,6 +133,8 @@ class App extends React.Component<Props, State> {
<Route exact path={'/novels'} component={Novels}/>
<Route exact path={'/novels/:novelId'} component={Novel}/>
<Route exact path={'/clubs/:clubId'} component={Club}/>
<Route exact path={'/users'} component={Users}/>
<Route exact path={'/user/:userId'} component={User}/>
<Route exact path={'/user/:userId/novels'} component={UserNovels}/>
@@ -138,7 +163,8 @@ class App extends React.Component<Props, State> {
export default connect(
(state: StoreState) => ({
notifications: state.notifications,
modal: state.modal
modal: state.modal,
snowfall: state.snowfall
}),
dispatch => ({
setUser: (userData: UserType) => dispatch(setUser(userData)),
+1 -3
View File
@@ -15,12 +15,10 @@
margin-top: 1rem
.Character__Image
width: 150px !important
width: 150px
height: 250px
margin-right: .5rem
border-radius: $borderRadius
background-size: cover
background-position: center
.Character__Info
display: flex
+1 -1
View File
@@ -38,7 +38,7 @@ class Character extends React.Component<Props, State> {
return (
<div className={'Character'}>
<div className="Character__Main">
<div style={{ backgroundImage: `url(${char.image})` }} className="Character__Image"/>
<img src={char.image} alt={char.original_name} className="Character__Image" />
<div className="Character__Info">
<div className="Character__Info_Name">
{char.original_name !== char.localized_name ? (
+37
View File
@@ -0,0 +1,37 @@
import React from "react";
import { getClub } from "../../api/clubs";
import Loading from "../../ui/Loading";
import Group from "../../ui/Group/Group";
type Props = {
match: { params: { clubId: number } }
};
type State = {
club: ClubType | null
}
class Club extends React.Component<Props, State> {
state: State = {
club: null
}
componentDidMount() {
getClub(this.props.match.params.clubId).then(club => {
this.setState({ club });
});
}
render() {
if(!this.state.club) return <Loading/>;
return (
<div>
<Group title={this.state.club.name}>
</Group>
</div>
)
}
}
export default Club;
-2
View File
@@ -43,8 +43,6 @@ class LoginModal extends React.Component<Props, State> {
const { login: log, password, recaptcha } = this.state;
login(log, password, recaptcha).then(user => {
if(!user) return;
setUser(user);
const successful = (
<NotificationMessage level={"success"} position={"center"}>
+69 -10
View File
@@ -4,19 +4,28 @@ import React, { FormEvent } from "react";
import { updateUserAppearanceSettings } from "../../../api/users";
import { connect } from "react-redux";
import { setUser } from "../../../store/ducks/currentUser";
import { setSnowflakeCount, setSnowfallStatus } from "../../../store/ducks/snowfall";
type Props = {
setUser: SetUser
settings: UserSettingsType
snowfallSettings: Snowfall
setSnowflakeCount: SetSnowflakeCount
setSnowfallStatus: SetSnowflakeStatus
}
type State = {
theme: Theme
colorName: boolean
snowflakeCount: 0
}
class Appearance extends React.Component<Props, State> {
state: State = {
theme: this.props.settings.theme
theme: this.props.settings.theme,
colorName: JSON.parse(localStorage.getItem('color-name') || 'false'),
snowflakeCount: JSON.parse(localStorage.getItem('snowflakeCount') ?? '250')
}
saveTheme = (event: FormEvent) => {
@@ -25,18 +34,19 @@ class Appearance extends React.Component<Props, State> {
const { theme } = this.state;
const { setUser } = this.props;
localStorage.setItem('theme', theme);
document.body.setAttribute('theme', theme);
updateUserAppearanceSettings(theme).then(user => {
setUser(user);
localStorage.setItem('theme', theme);
document.body.setAttribute('theme', theme);
})
});
}
render() {
const { theme } = this.state;
const { snowfallSettings, setSnowflakeCount, setSnowfallStatus } = this.props;
const { theme, colorName } = this.state;
return (
return (<>
<Group title={'Тема'} className={'Settings_Group'}>
<form onSubmit={this.saveTheme}>
<select onChange={e => this.setState({ theme: e.target.value as Theme })} value={theme}>
@@ -49,15 +59,64 @@ class Appearance extends React.Component<Props, State> {
<Button>Сохранить</Button>
</form>
</Group>
)
<Group title={'Остальное'}>
<label>
Выделять ник цветом группы
<input
type="checkbox"
onChange={e => {
localStorage.setItem('color-name', JSON.stringify(e.target.checked));
this.setState({ colorName: e.target.checked });
}}
checked={colorName}
/>
</label>
</Group>
<Group title={'Снег'}>
<label>
Включить снег
<input
type={'checkbox'}
checked={snowfallSettings.snowfallStatus}
onChange={e => {
setSnowfallStatus(e.target.checked);
localStorage.setItem('snowfallStatus', JSON.stringify(e.target.checked));
}}
/>
</label>
<label style={{ display: "flex", flexDirection: "column" }}>
<div>
Количество снежинок {snowfallSettings.snowflakeCount}
{snowfallSettings.snowflakeCount > 700 && (
<span style={{ color: 'red' }}> (использование большого количества снежинок может негативно сказаться на производительности!)</span>
)}
</div>
<input
type="range"
min={0}
max={1000}
value={snowfallSettings.snowflakeCount}
onChange={e => {
const value = JSON.parse(e.target.value);
setSnowflakeCount(value);
localStorage.setItem('snowflakeCount', value);
}}
/>
</label>
</Group>
</>);
}
}
export default connect(
(state: StoreState) => ({
settings: state.userSettings
settings: state.userSettings,
snowfallSettings: state.snowfall
}),
dispatch => ({
setUser: (user: UserType) => dispatch(setUser(user))
setUser: (user: UserType) => dispatch(setUser(user)),
setSnowflakeCount: (count: number) => dispatch(setSnowflakeCount(count)),
setSnowfallStatus: (status: boolean) => dispatch(setSnowfallStatus(status))
})
)(Appearance);
+17 -4
View File
@@ -41,16 +41,30 @@
font-weight: 600
&_Group
background-color: #0008
border-color: #eeeeee
border-width: 1px 1px 1px 4px
border-width: 1px
border-style: solid
color: var(--main-color)
border-radius: 4px
border-radius: 2px
padding: .2rem
text-align: center
font-size: 1.1rem
font-weight: 300
position: relative
z-index: 1
&::before
content: ''
width: 100%
border-top-color: inherit
border-top-width: 27px
border-top-style: solid
opacity: 0.5
position: absolute
z-index: -1
top: 0
left: 0
&_Avatar
background-size: cover
@@ -80,7 +94,6 @@
@media (max-width: 460px)
width: calc(100% - 1rem)
padding: .5rem 0
margin: 0 .5rem
+20 -12
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { CSSProperties } from "react";
import { fetchUser, fetchUserNovels, generateActivateLink } from "../../api/users";
import './User.sass'
import NotFoundError from "../NotFoundError";
@@ -78,7 +78,9 @@ class User extends React.Component<Props, State> {
if(errorCode === 100) return <NotFoundError/>;
if(!user) return <Loading/>;
const style = !user.is_activated ? { marginTop: '1rem' } : {}
const style: CSSProperties = !user.is_activated ? { marginTop: '1rem' } : {};
const userNameStyle: CSSProperties = JSON.parse(localStorage.getItem('color-name') || 'false') && user.group.id !== 0 ? { color: user.group.color } : {};
console.log(userNameStyle, localStorage.getItem('color-name'));
return (
<div className={'User'}>
@@ -88,8 +90,12 @@ class User extends React.Component<Props, State> {
<p>Активируйте аккаунт, для доступа ко многим функциям. Так же актвация аккаунта позволит
восстановить пароль.</p>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Button onClick={this.sendActivateLink}
style={{ fontSize: '1.2rem' }}>Активировать!</Button>
<Button
onClick={this.sendActivateLink}
style={{ fontSize: '1.2rem' }}
>
Активировать!
</Button>
</div>
</div>
)}
@@ -101,22 +107,24 @@ class User extends React.Component<Props, State> {
<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_Nick' style={userNameStyle}>{user.username}</div>
<div className='User__Info_Group' style={{
borderColor: user.group.color,
color: user.group.color
}}>{user.group.name}</div>
borderColor: user.group.color
}}>
{user.group.name}
</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_Nick' style={userNameStyle}>{user.username}</div>
<div className='User__Info_Group' style={{
borderColor: user.group.color,
color: user.group.color
}}>{user.group.name}</div>
borderColor: user.group.color
}}>
{user.group.name}
</div>
</div>
</div>
)}
+4 -1
View File
@@ -3,5 +3,8 @@ import currentUser from './currentUser'
import modal from "./modal";
import notifications from './notifications'
import userSettings from './userSettings';
import snowfall from './snowfall';
export default combineReducers({ currentUser, modal, notifications, userSettings })
export default combineReducers({
currentUser, modal, notifications, userSettings, snowfall
});
+37
View File
@@ -0,0 +1,37 @@
const SET_SNOWFLAKE_COUNT = 'unmei/snowfall/SET_SNOWFLAKE_COUNT';
const SET_SNOWFALL_STATUS = 'unmei/snowfall/SET_SNOWFALL_STATUS';
const initialState: Snowfall = {
snowflakeCount: JSON.parse(localStorage.getItem('snowflakeCount') ?? '250'),
snowfallStatus: true
};
const reducer = (state = initialState, action: Action<Snowfall>) => {
if(!action.payload) return state;
const payload = action.payload;
switch(action.type) {
case SET_SNOWFLAKE_COUNT:
localStorage.setItem('snowflakeCount', JSON.stringify(payload.snowflakeCount));
return Object.assign({}, state, { snowflakeCount: payload.snowflakeCount });
case SET_SNOWFALL_STATUS:
return Object.assign({}, state, { snowfallStatus: payload.snowfallStatus });
default:
return state
}
}
const setSnowflakeCount = (snowflakeCount: number) => ({
type: SET_SNOWFLAKE_COUNT,
payload: {
snowflakeCount
}
});
const setSnowfallStatus = (snowfallStatus: boolean) => ({
type: SET_SNOWFALL_STATUS,
payload: {
snowfallStatus
}
});
export default reducer;
export { setSnowflakeCount, setSnowfallStatus }
+15
View File
@@ -1,3 +1,18 @@
type Theme = 'dark' | 'blue' | 'red' | 'green' | 'light' | 'win95';
type ExitStatus = 'came_out';
type NovelPlatform = 'win';
type ApiError = {
code: number
text: string
}
type ApiResponse<T> = {
error: boolean
error_data?: ApiError | string
data?: T
}
type VersionResponse = {
version: string
}
+8
View File
@@ -1,8 +1,14 @@
type Snowfall = {
snowflakeCount: number
snowfallStatus: boolean
};
type StoreState = {
currentUser: UserType
userSettings: UserSettingsType
notifications: React.ReactNode[]
modal: React.ReactNode
snowfall: Snowfall
}
type Action<T> = {
@@ -16,3 +22,5 @@ type SetSettings = (settings: UserSettingsType) => void
type SetModal = (modal: React.ReactNode | null) => void
type HideModal = () => void
type AddNotification = (notification: React.ReactNode) => void
type SetSnowflakeCount = (snowflakeCount: number) => void
type SetSnowflakeStatus = (snowfallStatus: boolean) => void
+18 -25
View File
@@ -1,16 +1,14 @@
type Theme = 'dark' | 'blue' | 'red' | 'green' | 'light'
type ExitStatus = 'came_out'
type NovelPlatform = 'win'
type ApiError = {
code: number
text: string
}
type ApiResponse<T> = {
error: boolean
error_data?: ApiError | string
data?: T
type UserType = {
authorized: boolean
id: number
username: string
email?: string
avatar: string
cover: string
group: UserGroupType
is_superuser: boolean
is_activated: boolean
is_banned: boolean
}
type NovelType = {
@@ -36,18 +34,6 @@ type CharacterType = {
main: boolean
}
type UserType = {
authorized: boolean
id: number
username: string
email?: string
avatar: string
cover: string
group: UserGroupType
is_superuser: boolean
is_activated: boolean
}
type UserSettingsType = {
avatar: string
theme: Theme
@@ -89,3 +75,10 @@ type PostType = {
author: string
author_id: number
}
type ClubType = {
id: number
name: string
avatar: string
ownerId: number
}
+2 -2
View File
@@ -3,7 +3,7 @@ import classNames from "../../classNames";
import './Group.sass'
interface Props extends HTMLAttributes<HTMLElement> {
title: string
title?: string
direction?: 'column' | 'row'
}
@@ -18,7 +18,7 @@ const Group = (restProps: Props) => {
}
return (
<div className={classNames('Group', className)} {...props}>
<div className="Group__Title">{title}</div>
{title && <div className="Group__Title">{title}</div>}
<div style={style} className={'Group__Content'}>
{children}
</div>
+416 -544
View File
File diff suppressed because it is too large Load Diff