This commit is contained in:
2020-06-24 18:56:24 +03:00
parent 9447ee613b
commit 5d4972145b
119 changed files with 5412 additions and 5086 deletions
+16
View File
@@ -0,0 +1,16 @@
.BBCodes
font-size: 1.3rem
margin-bottom: .2rem
svg
cursor: pointer
padding: 0
svg:first-child
padding-right: .5rem
svg:last-child
padding-left: .5rem
svg:not(:first-child):not(:last-child)
padding: 0 .5rem
+51
View File
@@ -0,0 +1,51 @@
import React, {useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faBold,
faExclamationTriangle,
faItalic,
faStrikethrough,
faTint,
faUnderline
} from "@fortawesome/free-solid-svg-icons";
import './BBEditor.sass';
import TextField from "../TextField/TextField";
interface Props extends React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement> {
inputRef: React.RefObject<HTMLTextAreaElement>
}
const BBEditor = ({ inputRef, ref, ...props }: Props) => {
const [text, setText] = useState('');
const addTag = (event: React.MouseEvent, tag: string, defaultValue?: string) => {
event.preventDefault();
const [start, end] = [inputRef.current!!.selectionStart, inputRef.current!!.selectionEnd];
const openTag = `${tag}${defaultValue ? `=${defaultValue}` : ''}`;
setText(`${text.slice(0, start)}[${openTag}]${text.slice(start, end)}[/${tag}]${text.slice(end, text.length)}`)
inputRef.current!!.focus();
}
return (
<div style={{ width: '100%' }}>
<div className="BBCodes">
<FontAwesomeIcon onClick={event => addTag(event, 'b')} icon={faBold}/>
<FontAwesomeIcon onClick={event => addTag(event, 'i')} icon={faItalic}/>
<FontAwesomeIcon onClick={event => addTag(event, 'u')} icon={faUnderline}/>
<FontAwesomeIcon onClick={event => addTag(event, 's')} icon={faStrikethrough}/>
<FontAwesomeIcon onClick={event => addTag(event, 'spoiler')} icon={faExclamationTriangle}/>
<FontAwesomeIcon onClick={event => addTag(event, 'color', 'white')} icon={faTint}/>
</div>
<TextField
ref={inputRef}
style={{ width: '100%' }}
value={text}
onChange={event => setText(event.target.value)}
{...props}
/>
</div>
)
}
export default BBEditor;
+17
View File
@@ -0,0 +1,17 @@
@import "../../Constants"
.Button
cursor: pointer
padding: .2rem
border-radius: $borderRadius
background-color: var(--block-color)
border: 1px solid var(--main-color)
color: var(--main-color)
&:disabled
color: var(--sec-color)
&:hover
background-color: var(--block-sec-color)
color: var(--link-color)
border: 1px solid var(--link-color)
+14
View File
@@ -0,0 +1,14 @@
import React, {ButtonHTMLAttributes} from "react";
import './Button.sass';
import classNames from "../../classNames";
const Button = (restProps: ButtonHTMLAttributes<HTMLElement>) => {
const { className, children, ...props } = restProps;
return (
<button className={classNames('Button', className)} {...props}>
{children}
</button>
)
}
export default Button;
+21
View File
@@ -0,0 +1,21 @@
@import "../../Constants"
.Comment
display: flex
background-color: var(--block-color)
border-radius: $borderRadius
padding: $blockPadding * 2
&:not(:last-child)
margin-bottom: $blockPadding
.Comment__User_Avatar
border-radius: 50%
width: 48px
height: 48px
margin-right: .5rem
&__Content
.Comment__User_Username
font-size: 1.1rem
font-weight: 600
+30
View File
@@ -0,0 +1,30 @@
import React from "react";
import './Comment.sass'
import {Link} from "react-router-dom";
// @ts-ignore
import parser from 'bbcode-to-react';
import Loading from "../Loading";
type Props = {
user: UserType | undefined
text: string
}
class Comment extends React.Component<Props> {
render() {
const { user, text } = this.props;
if(!user) return <Loading/>;
return (user &&
<div className={'Comment'}>
<img className={'Comment__User_Avatar'} src={user.avatar} alt={user.username}/>
<div className="Comment__Content">
<Link to={`/user/${user.id}`} className="Comment__User_Username">{user.username}</Link>
<pre className={'Comment__Text'}>{parser.toReact(text)}</pre>
</div>
</div>
);
}
}
export default Comment;
+7
View File
@@ -0,0 +1,7 @@
.Buttons
display: flex
justify-content: flex-end
.Button
margin: 1rem .5rem 0
font-size: 1.3rem
+53
View File
@@ -0,0 +1,53 @@
import React from "react";
import Button from "../Button/Button";
import './ConfirmPopout.sass'
import '../Modal/Modal.sass';
import {connect} from "react-redux";
import {hideModal} from "../../store/actions";
type Props = {
hideModal: () => void
onConfirm?: () => void
onDismiss?: () => void
title?: string
children: any
}
class ConfirmPopout extends React.Component<Props> {
onConfirm = () => {
const { hideModal, onConfirm } = this.props;
if(onConfirm) onConfirm();
hideModal();
}
onDismiss = () => {
const { hideModal, onDismiss } = this.props;
if(onDismiss) onDismiss();
hideModal();
}
render() {
const { children, title } = this.props;
return (
<div className="Modal__Overlay">
<div className={"Modal"}>
{title && <div className="Modal__Header">
{title}
</div>}
{children}
<div className={'Buttons'}>
<Button onClick={this.onConfirm}>Да</Button>
<Button onClick={this.onDismiss}>Нет</Button>
</div>
</div>
</div>
)
}
}
export default connect(null,
dispatch => ({
hideModal: () => dispatch(hideModal())
})
)(ConfirmPopout);
+11
View File
@@ -0,0 +1,11 @@
@import "../../Constants"
.Group
background-color: var(--sec-bg-color)
border-radius: $borderRadius
padding: .5rem
&__Title
font-size: 1.2rem
font-weight: 600
margin-bottom: .5rem
+29
View File
@@ -0,0 +1,29 @@
import React, {HTMLAttributes} from "react";
import classNames from "../../classNames";
import './Group.sass'
interface Props extends HTMLAttributes<HTMLElement> {
title: string
direction?: 'column' | 'row'
}
const Group = (restProps: Props) => {
const { title, direction, className, children, ...props } = restProps;
let style: React.CSSProperties = {};
if(direction === 'column') {
style = {
display: 'flex',
flexDirection: 'column'
}
}
return (
<div className={classNames('Group', className)} {...props}>
<div className="Group__Title">{title}</div>
<div style={style}>
{children}
</div>
</div>
)
}
export default Group;
+8
View File
@@ -0,0 +1,8 @@
.Input
background-color: var(--sec-block-color) !important
border: none
border-bottom: 1px solid var(--main-color) !important
color: var(--main-color)
&:disabled
color: var(--sec-color)
+12
View File
@@ -0,0 +1,12 @@
import React, {InputHTMLAttributes} from "react";
import classNames from "../../classNames";
import './Input.sass';
const Input = (restProps: InputHTMLAttributes<HTMLElement>) => {
const { className, ...props } = restProps;
return (
<input className={classNames('Input', className)} {...props} />
)
}
export default Input;
+11
View File
@@ -0,0 +1,11 @@
import React from 'react';
// @ts-ignore
import Loader from 'react-loader-spinner'
const Loading: React.FC = () => (
<div style={{ textAlign: 'center' }}>
<Loader color='#aaa' type='Rings' style={{ height: '100%' }} />
</div>
);
export default Loading;
+38
View File
@@ -0,0 +1,38 @@
.Modal__Overlay
display: flex
align-items: center
justify-content: center
position: fixed
top: 0
right: 0
left: 0
bottom: 0
background-color: #0f0f0fe9
z-index: 9999
opacity: 1
overflow: hidden
.Modal
background-color: var(--main-bg-color)
padding: .5rem
.Modal__Header
display: flex
justify-content: space-between
align-items: center
font-size: 1.4rem
font-weight: 700
margin-bottom: .5rem
.Modal__CloseButton
background: none
font-size: 2rem
padding: 0
height: 2.5rem
width: 2.5rem
border: none
color: var(--main-color)
cursor: pointer
&:hover
color: var(--link-color)
+66
View File
@@ -0,0 +1,66 @@
import React from "react";
import './Modal.sass'
import {connect} from "react-redux";
import {setModal} from "../../store/actions";
type Props = {
setModal: (modal: React.ReactNode | null) => void
children: any
className?: string
title?: string
}
class Modal extends React.Component<Props> {
private modal: HTMLDivElement | null | undefined;
componentDidMount() {
window.addEventListener('keyup', this.handleKeyUp, false);
document.addEventListener('mousedown', this.handleOutsideClick, false);
}
componentWillUnmount() {
window.removeEventListener('keyup', this.handleKeyUp, false);
document.removeEventListener('mousedown', this.handleOutsideClick, false);
}
handleKeyUp = (e: KeyboardEvent) => {
if(e.code === 'Escape') {
const { setModal } = this.props;
e.preventDefault();
setModal(null);
}
};
handleOutsideClick = (e: any) => {
if(this.modal && !this.modal.contains(e.target)) {
const { setModal } = this.props;
setModal(null);
}
};
render() {
const { setModal, children, title } = this.props;
return (
<div className="Modal__Overlay">
<div className={"Modal"} ref={node => (this.modal = node)}>
<div className={this.props.className}>
<div className="Modal__Header">
{title && title}
<button className={"Modal__CloseButton"} onClick={() => setModal(null)}>X</button>
</div>
{children}
</div>
</div>
</div>
);
}
}
export default connect(null,
dispatch => ({
setModal: (modal: React.ReactNode | null) => {
dispatch(setModal(modal))
}
})
)(Modal);
+17
View File
@@ -0,0 +1,17 @@
.Post
background-color: var(--block-color)
padding: .5rem
&__Title
font-size: 1.2rem
&__Content
border-top: var(--link-color) 1px solid
border-bottom: var(--link-color) 1px solid
&__Footer
display: flex
justify-content: space-between
&:not(:last-child)
margin-bottom: 1rem
+35
View File
@@ -0,0 +1,35 @@
import React from "react";
// @ts-ignore
import parser from 'bbcode-to-react';
import classNames from "../../classNames";
import './NewsPost.sass';
interface Props {
title: string
short_post: string
author_id: number
author: string
date: Date
className?: string
onClick?: () => void
}
const NewsPost = (restProps: Props) => {
const { title, short_post, author, author_id, className, onClick, ...props } = restProps;
const date = new Date(restProps.date);
return (
<div className={classNames('Post', className)} {...props} onClick={onClick}>
<div className="Post__Title">{title}</div>
<pre className='Post__Content'>{parser.toReact(short_post)}</pre>
<div className="Post__Footer">
<div>
Автор: {author}
</div>
<div>Дата: {date.toLocaleDateString()}, {date.toLocaleTimeString()}</div>
</div>
</div>
)
}
export default NewsPost;
+27
View File
@@ -0,0 +1,27 @@
@import "../../Constants"
.Animate-Notification-exit
opacity: 0
.Notification
background-color: var(--block-color)
border: 1px solid var(--link-color)
padding: .5rem
box-sizing: border-box
max-width: 500px
width: 500px
cursor: pointer
margin-top: 1rem
&--Error
background: var(--error-bg-color)
&--Success
background: var(--success-bg-color)
&:not(:last-child)
margin-bottom: .5rem
.Progress
height: 5px
background: white
+73
View File
@@ -0,0 +1,73 @@
import React from "react";
import './NotificationMessage.sass'
import {capitalize} from "../../utils";
type Props = {
children: React.ReactNode
level?: 'info' | 'success' | 'error'
}
type State = {
time: number
paused: boolean
};
class NotificationMessage extends React.Component<Props, State> {
timer: NodeJS.Timeout | undefined;
state = {
time: 5000, paused: false
};
onMouseOver = () => {
this.setState({ paused: true });
};
onMouseOut = () => {
this.setState({ paused: false });
};
onClick = () => {
this.timer = setInterval(() => this.interval(80), 1);
this.setState({ paused: false });
};
interval = (step=50) => {
const { time, paused } = this.state;
if(time <= 0 && this.timer) clearInterval(this.timer);
if(!paused) this.setState({ time: time-step });
};
componentDidMount(): void {
this.timer = setInterval(() => this.interval(10), 1)
};
componentWillUnmount(): void {
if(this.timer) clearTimeout(this.timer);
}
render() {
const { time } = this.state;
const { children, level } = this.props;
if(time <= 0)
return null;
const styleClass = level ? `Notification--${capitalize(level)}` : '';
const width = window.screen.width >= 500 ? 500 : window.screen.width;
return (
<div
className={`Notification ${styleClass}`}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
onClick={this.onClick}
style={{ width }}
>
{children}
<div className="Progress" style={{ width: `${time / 5000 * 100}%` }}/>
</div>
);
}
}
export default NotificationMessage;
+58
View File
@@ -0,0 +1,58 @@
@import '../../Constants'
.NovelItem
background-color: var(--block-color)
padding-bottom: $blockPadding
border-radius: $borderRadius
margin: .5rem
@media (min-width: 601px)
width: $novelBlockSize
@media (max-width: 600px)
width: 100%
display: flex
&.Grid
flex-direction: column
align-items: center
&.Table
width: 100%
padding: .2rem
box-sizing: border-box
flex-direction: row
justify-content: space-between
align-items: center
&:not(:last-child)
margin: 0 0 .5rem
&:last-child
margin: 0
.NovelItem__Status
white-space: nowrap
&__Image
border-radius: $borderRadius $borderRadius 0 0
background-size: cover
background-position: center
width: 100%
height: 130px
&__Title
width: 100%
text-align: center
display: inline-block
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
padding: .2rem
box-sizing: border-box
font-weight: 400
&__Status
font-weight: 300
display: flex
+41
View File
@@ -0,0 +1,41 @@
import React from 'react';
import { TranslateStatus } from '../../api/api';
import { Link } from 'react-router-dom';
import './NovelItem.sass';
import {capitalize, generateClassName} from "../../utils";
type Props = {
id: number
original_name: string
viewType: string
novelId?: number
localized_name?: string
image?: string
status?: string
mark?: number
}
const NovelItem = ({ id, original_name, localized_name, image, status, mark, viewType, novelId }: Props) => (
<Link to={`/novels/${id}`} className={generateClassName("NovelItem", capitalize(viewType) || 'Grid')}>
{viewType === 'grid' && (<>
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
<div className="NovelItem__Title">
{localized_name || original_name}
</div>
{status && <div className="NovelItem__Status">
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
</div>}
</>)}
{viewType === 'table' && (<>
<div className="NovelItem__Component">{novelId || ''}</div>
<div className="NovelItem__Title">
{localized_name || original_name}
</div>
{status && <div className="NovelItem__Status">
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
</div>}
</>)}
</Link>
);
export default NovelItem;
+62
View File
@@ -0,0 +1,62 @@
import React from "react";
type Props = {
sitekey: string
theme?: 'dark' | 'light'
onVerify?: (res: string) => void
onError?: (error: string) => void
onExpired?: () => void
}
type State = {
isReady: boolean
}
class Recaptcha extends React.Component<Props, State> {
private readonly interval: NodeJS.Timeout;
state: State = {
isReady: false
}
constructor(props: Props) {
super(props);
this.interval = setInterval(() => {
// @ts-ignore
if(typeof window?.grecaptcha?.render === 'function') {
clearInterval(this.interval);
this.setState({ isReady: true })
}
}, 100)
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any) {
const { isReady: isReadyPrev } = prevState;
const { isReady } = this.state;
if(!isReadyPrev && isReady) {
const { sitekey, theme, onExpired, onVerify } = this.props;
// @ts-ignore
window.grecaptcha.render(document.getElementById('g-recaptcha'), {
sitekey, theme,
callback: onVerify || undefined,
'expired-callback': onExpired || undefined
})
}
}
reset = () => {
// @ts-ignore
if(typeof window?.grecaptcha?.reset === "function") window.grecaptcha.reset(document.getElementById('g-recaptcha'));
}
render() {
return (
<div
id="g-recaptcha"
/>
)
}
}
export default Recaptcha;
+8
View File
@@ -0,0 +1,8 @@
@import "../../Constants"
.Spoiler
cursor: pointer
&:hover
color: var(--link-color)
text-decoration: underline
+33
View File
@@ -0,0 +1,33 @@
import React from "react";
import './Spoiler.sass'
type Props = {
children?: React.ReactNode
}
type State = {
expand: boolean
}
class Spoiler extends React.Component<Props, State> {
state = {
expand: false
};
render() {
const { expand } = this.state;
const { children } = this.props;
return (
<div
className="Spoiler"
onClick={() => this.setState({ expand: !expand })}
style={{ fontStyle: expand ? 'normal' : 'italic' }}
>
{expand ? children : '[Спойлер]'}
</div>
)
}
}
export default Spoiler;
+13
View File
@@ -0,0 +1,13 @@
// @ts-ignore
import { Tag } from 'bbcode-to-react';
import Spoiler from "./Spoiler";
import React from 'react';
class SpoilerTag extends Tag {
toReact() {
// @ts-ignore
return (<Spoiler>{this.getContent(true)}</Spoiler>)
}
}
export default SpoilerTag;
+16
View File
@@ -0,0 +1,16 @@
// @ts-ignore
import { Tag } from 'bbcode-to-react';
import React from 'react';
class ColorTag extends Tag {
toReact() {
// @ts-ignore
const text = this.getContent(true);
// @ts-ignore
const color = this.params.color;
// @ts-ignore
return <span style={{ color }}>{text}</span>;
}
}
export default ColorTag;
+11
View File
@@ -0,0 +1,11 @@
.TextField
color: var(--main-color)
background-color: var(--block-color)
font-size: 1rem
padding: .25rem
border: none
border-radius: .2rem
box-sizing: border-box
&:disabled
color: var(--sec-color)
+13
View File
@@ -0,0 +1,13 @@
import React, {TextareaHTMLAttributes} from "react";
import classNames from "../../classNames";
import './TextField.sass';
const TextField = React.forwardRef(
(restProps: React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, ref: React.Ref<HTMLTextAreaElement>) => {
const { className, ...props } = restProps;
return (
<textarea className={classNames('TextField', className)} ref={ref} {...props} />
)
})
export default TextField;
+3
View File
@@ -0,0 +1,3 @@
.Title
font-size: 1.2rem
font-weight: 600
+8
View File
@@ -0,0 +1,8 @@
import React from "react";
import './Title.sass';
const Title = ({ children }: any) => (
<div className={'Title'}>{children}</div>
)
export default Title;