admin panel; footer module; select
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.2",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"@sentry/react": "6.2.0",
|
||||
"@sentry/tracing": "6.2.0",
|
||||
"bbcode-to-react": "0.2.9",
|
||||
"eruda": "2.4.1",
|
||||
"eruda-dom": "2.0.0",
|
||||
|
||||
+3
-2
@@ -61,7 +61,7 @@ const TranslatePlatform: { [id: string]: string } = {
|
||||
win: 'Windows',
|
||||
mac: 'macOS',
|
||||
linux: 'Linux',
|
||||
ios: 'IOS',
|
||||
ios: 'iOS',
|
||||
android: 'Andriod'
|
||||
};
|
||||
|
||||
@@ -70,6 +70,7 @@ const TranslateExitStatus: { [id: string]: string } = {
|
||||
};
|
||||
|
||||
export const getVersion = () => get<VersionResponse>('version');
|
||||
export const version = '1.2';
|
||||
export const version = '1.2.2';
|
||||
export const build = 121;
|
||||
|
||||
export { get, post, put, del, TranslateStatus, TranslatePlatform, TranslateExitStatus };
|
||||
|
||||
+2
-1
@@ -5,10 +5,11 @@ export const fetchNovels = (orderBy?: string) => get<NovelType[]>(orderBy ? `nov
|
||||
export const createNovel = (novel: NovelType) => post<NovelType>('novels', { ...novel });
|
||||
|
||||
export const fetchNovel = (id: number) => get<NovelType>(`novels/${id}`);
|
||||
export const updateNovel = (id: number, novel: NovelType) => put<NovelType>(`novels/${id}`, { ...novel });
|
||||
export const updateNovel = (novel: NovelType) => put<NovelType>(`novels/${novel.id}`, novel);
|
||||
|
||||
export const fetchNovelCharacters = (id: number) => get<CharacterType[]>(`novels/${id}/characters`);
|
||||
|
||||
export const fetchGenres = () => get<GenreType[]>('genres');
|
||||
export const fetchNovelGenres = (id: number) => get<GenreType[]>(`novels/${id}/genres`);
|
||||
|
||||
export const fetchNovelComments = (id: number, offset = 0, count = 5) => get<CommentsResponse>(`novels/${id}/comments?offset=${offset}&count=${count}`);
|
||||
|
||||
@@ -55,5 +55,8 @@
|
||||
|
||||
.Footer__Bottom
|
||||
border-top: 1px solid var(--main-color)
|
||||
display: flex
|
||||
justify-content: center
|
||||
flex-direction: column
|
||||
padding: 2rem 0
|
||||
text-align: center
|
||||
text-align: center
|
||||
@@ -1,36 +1,46 @@
|
||||
import React from 'react';
|
||||
import './Footer.sass';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import styles from './Footer.module.sass';
|
||||
import { Link } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faDiscord, faTelegramPlane, faVk } from '@fortawesome/free-brands-svg-icons';
|
||||
import { build, getVersion, version } from "../../api/api";
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const [backend, setVersion] = useState<VersionResponse>({ version: '', build: 0 });
|
||||
useMemo(() => {
|
||||
getVersion().then(setVersion);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='Footer'>
|
||||
<footer className='Footer__Tabs'>
|
||||
<div className="Footer__Tab">
|
||||
<div className="Footer__Tab_Title">Команда</div>
|
||||
<div className={styles.Footer}>
|
||||
<footer className={styles.Footer__Tabs}>
|
||||
<div className={styles.Footer__Tab}>
|
||||
<div className={styles.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>
|
||||
<div className={styles.Footer__Tab}>
|
||||
<div className={styles.Footer__Tab_Title}>Соцсети</div>
|
||||
|
||||
<div className="Links">
|
||||
<div className={styles.Links}>
|
||||
<a href="https://vk.com/unmei_site">
|
||||
<div className="Link"><FontAwesomeIcon icon={faVk}/></div>
|
||||
<div className={styles.Link}><FontAwesomeIcon icon={faVk}/></div>
|
||||
</a>
|
||||
<a href="https://discord.gg/4CA8Cju">
|
||||
<div className="Link"><FontAwesomeIcon icon={faDiscord}/></div>
|
||||
<div className={styles.Link}><FontAwesomeIcon icon={faDiscord}/></div>
|
||||
</a>
|
||||
<a href="https://t.me/unmei_site">
|
||||
<div className="Link"><FontAwesomeIcon icon={faTelegramPlane}/></div>
|
||||
<div className={styles.Link}><FontAwesomeIcon icon={faTelegramPlane}/></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<div className="Footer__Bottom">
|
||||
Unmei © 2019-2021
|
||||
<div className={styles.Footer__Bottom}>
|
||||
<div>Unmei © 2019-2021</div>
|
||||
<div>
|
||||
Frontend version: <span style={{ color: backend.build == build ? "green" : "red" }}>{version} ({build})</span>;
|
||||
Backend version: <span style={{ color: backend.build == build ? "green" : "red" }}>{backend.version} ({backend.build})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,13 +10,15 @@ import APAddPost from "./News/APAddPost";
|
||||
import APNovels from "./Novels/APNovels";
|
||||
import APUsers from "./Users/APUsers";
|
||||
import APModifyUser from "./Users/APModifyUser";
|
||||
import APModifyNovel from "./Novels/APModifyNovel";
|
||||
import APMain from "./Main/APMain";
|
||||
|
||||
type Props = {
|
||||
user: UserType
|
||||
match: { path: string }
|
||||
}
|
||||
|
||||
type State = {}
|
||||
type State = {};
|
||||
|
||||
class AdminPanel extends React.Component<Props, State> {
|
||||
render() {
|
||||
@@ -26,8 +28,7 @@ class AdminPanel extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<div className={'AdminPanel'}>
|
||||
<div className="AdminPanel__Title">Админ панель <span style={{ color: 'var(--error-bg-color)' }}>(в разработке)</span>
|
||||
</div>
|
||||
<div className="AdminPanel__Title">Админ панель</div>
|
||||
<div className="AdminPanel__Router">
|
||||
<Link to={path}>Главная</Link>
|
||||
<Link to={`${path}/users`}>Пользователи</Link>
|
||||
@@ -36,13 +37,15 @@ class AdminPanel extends React.Component<Props, State> {
|
||||
</div>
|
||||
|
||||
<Switch>
|
||||
<Route exact path={path} component={APMain}/>
|
||||
|
||||
<Route exact path={`${path}/news`} component={APNews}/>
|
||||
<Route exact path={`${path}/news/new`} component={APAddPost}/>
|
||||
<Route exact path={`${path}/news/:id`} component={APModifyPost}/>
|
||||
|
||||
<Route exact path={`${path}/novels`} component={APNovels}/>
|
||||
<Route exact path={`${path}/novels/new`} component={NotFoundError}/>
|
||||
<Route exact path={`${path}/novels/:id`} component={NotFoundError}/>
|
||||
<Route exact path={`${path}/novels/:id`} component={APModifyNovel}/>
|
||||
|
||||
<Route exact path={`${path}/users`} component={APUsers}/>
|
||||
<Route exact path={`${path}/users/new`} component={NotFoundError}/>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import Group from "../../../ui/Group/Group";
|
||||
import { build, getVersion, version } from "../../../api/api";
|
||||
import Loading from "../../../components/Loading";
|
||||
|
||||
type State = {
|
||||
backend: VersionResponse | null
|
||||
}
|
||||
|
||||
class APMain extends React.Component<any, State> {
|
||||
state: State = {
|
||||
backend: null
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
getVersion().then(backend => this.setState({ backend }));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { backend } = this.state;
|
||||
|
||||
if(!backend) return <Loading/>;
|
||||
return (
|
||||
<Group title={'Основное'}>
|
||||
<div>
|
||||
Текущая версия сайта: <span style={{ color: backend.build == build ? "green" : "red" }}>{version} (билд: {build})</span>
|
||||
</div>
|
||||
<div>
|
||||
Текущая версия API: <span style={{ color: backend.build == build ? "green" : "red" }}>{backend.version} (билд: {backend.build})</span>
|
||||
</div>
|
||||
<div>
|
||||
Совпадение версий сборок: <span style={{ color: backend.build == build ? "green" : "red" }}>{backend.build == build ? 'да' : 'нет'}</span>
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default APMain;
|
||||
@@ -0,0 +1,142 @@
|
||||
import React, { ChangeEvent, FormEvent } from "react";
|
||||
import Group from "../../../ui/Group/Group";
|
||||
import NotFoundError from "../../../components/NotFoundError";
|
||||
import './APNovels.sass';
|
||||
import Input from "../../../ui/Input/Input";
|
||||
import Button from "../../../ui/Button/Button";
|
||||
import { connect } from "react-redux";
|
||||
import { setModal } from "../../../store/ducks/modal";
|
||||
import { addNotification } from "../../../store/ducks/notifications";
|
||||
import { fetchGenres, fetchNovel, updateNovel } from "../../../api/novels";
|
||||
// @ts-ignore
|
||||
import parser from 'bbcode-to-react';
|
||||
import TextField from "../../../ui/TextField/TextField";
|
||||
import Select from "../../../ui/Select/Select";
|
||||
|
||||
type Props = {
|
||||
match: { params: { id: number }, path: string }
|
||||
history: { push: (path: string) => void }
|
||||
setPopout: (modal: React.ReactNode | null) => void
|
||||
addNotification: (notification: React.ReactNode) => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
novel: NovelType | null
|
||||
genres: GenreType[]
|
||||
title: string
|
||||
};
|
||||
|
||||
class APModifyNovel extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
novel: null, genres: [],
|
||||
title: ''
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { match } = this.props;
|
||||
|
||||
fetchNovel(match.params.id).then(novel => {
|
||||
novel.release_date = new Date(novel.release_date);
|
||||
this.setState({ novel, title: novel.original_name });
|
||||
});
|
||||
fetchGenres().then(genres => this.setState({ genres }));
|
||||
}
|
||||
|
||||
changeOriginalTitle = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { novel } = this.state;
|
||||
if(!novel) return;
|
||||
|
||||
const newTitle = e.target.value;
|
||||
document.title = `${newTitle} / Админ-панель`;
|
||||
novel.original_name = newTitle;
|
||||
this.setState({ novel });
|
||||
}
|
||||
|
||||
changeLocalizedTitle = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { novel } = this.state;
|
||||
if(!novel) return;
|
||||
|
||||
novel.localized_name = e.target.value;
|
||||
this.setState({ novel });
|
||||
}
|
||||
|
||||
changeGenres = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const { novel } = this.state;
|
||||
if(!novel) return;
|
||||
|
||||
novel.genres = Array.from(e.target.selectedOptions).map(genre => {
|
||||
return this.state.genres.filter(g => g.name === genre.value)[0];
|
||||
});
|
||||
this.setState({ novel });
|
||||
}
|
||||
|
||||
saveNovel = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const { novel } = this.state;
|
||||
if(!novel) return;
|
||||
updateNovel(novel).catch(err => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { novel, genres, title } = this.state;
|
||||
|
||||
if(!novel) return <NotFoundError/>;
|
||||
return (
|
||||
<Group title={title}>
|
||||
<form className={'APNovel'} onSubmit={this.saveNovel}>
|
||||
<Input type="text" className={'APNovel_Input'} placeholder={'Оригинальное название'} value={novel.original_name} onChange={this.changeOriginalTitle}/>
|
||||
<Input type="text" className={'APNovel_Input'} placeholder={'Название на русском'} value={novel.localized_name} onChange={this.changeLocalizedTitle}/>
|
||||
|
||||
<TextField className={'APNovel_TextField'} placeholder={'Описание'} style={{ height: 100 }} value={novel.description} onChange={(e) => {
|
||||
novel.description = e.target.value;
|
||||
this.setState({ novel });
|
||||
}}/>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: "center" }}>
|
||||
<span style={{ marginRight: 8 }}>Жанры:</span>
|
||||
<Select
|
||||
multiple
|
||||
onChange={this.changeGenres}
|
||||
value={novel.genres.map(genre => genre.name)}
|
||||
>
|
||||
{genres.map(genre => (
|
||||
<option
|
||||
key={genre.id}
|
||||
value={genre.name}
|
||||
>
|
||||
{genre.localized_name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span style={{ marginRight: 8 }}>Дата выхода:</span>
|
||||
<input
|
||||
type="date"
|
||||
value={novel.release_date.toISOString().substring(0, 10)}
|
||||
onChange={e => {
|
||||
novel.release_date = new Date(e.target.value);
|
||||
this.setState({ novel });
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<Button style={{ marginRight: '1rem' }} className={'APNovel_Button'}>Сохранить</Button>
|
||||
<Button style={{ color: 'var(--error-bg-color)' }} className={'APNovel_Button'} type={"button"}>Удалить</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null,
|
||||
dispatch => ({
|
||||
setPopout: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
||||
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
||||
})
|
||||
)(APModifyNovel);
|
||||
@@ -0,0 +1,10 @@
|
||||
.APNovel
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
&_Input, &_Button, &_TextField
|
||||
font-size: 1.2rem
|
||||
margin: .5rem 0
|
||||
|
||||
&_TextField
|
||||
resize: vertical
|
||||
@@ -1,16 +1,47 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../../../ui/Button/Button";
|
||||
import Group from "../../../ui/Group/Group";
|
||||
import { fetchNovels } from "../../../api/novels";
|
||||
|
||||
type Props = {};
|
||||
type Props = {
|
||||
match: { path: string }
|
||||
};
|
||||
|
||||
type State = {};
|
||||
type State = {
|
||||
novels: NovelType[]
|
||||
};
|
||||
|
||||
class APNovels extends React.Component<Props, State> {
|
||||
render() {
|
||||
return (
|
||||
<div>Novels</div>
|
||||
)
|
||||
state: State = {
|
||||
novels: []
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetchNovels().then(novels => this.setState({ novels }));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { match: { path } } = this.props;
|
||||
const { novels } = this.state;
|
||||
|
||||
return (
|
||||
<Group title={'Новелы'}>
|
||||
<Link to={`${path}/new`}>
|
||||
<Button>Добавить новелу</Button>
|
||||
</Link>
|
||||
{novels.sort((a, b) => a.id-b.id).map(novel => (
|
||||
<Link to={`${path}/${novel.id}`} key={novel.id}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{novel.id}</div>
|
||||
<div>{novel.original_name}</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</Group>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -21,12 +21,12 @@ import AdminPanel from "../AdminPanel/AdminPanel";
|
||||
import PasswordRestoreGenerate from "../PasswordRestore/PasswordRestoreGenerate";
|
||||
import PasswordRestore from "../PasswordRestore/PasswordRestore";
|
||||
import Users from "../Users/Users";
|
||||
import { getVersion, version } from "../../api/api";
|
||||
import { build, getVersion, version } from "../../api/api";
|
||||
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 { hasPermission, isMobile } from "../../utils";
|
||||
import Club from "../Club/Club";
|
||||
import Snowfall from "react-snowfall";
|
||||
// @ts-ignore
|
||||
@@ -91,7 +91,7 @@ class App extends React.Component<Props, State> {
|
||||
document.body.setAttribute('theme', settings.theme);
|
||||
}
|
||||
});
|
||||
if(hasPermission(user, 'mobile_debug'))
|
||||
if(hasPermission(user, 'mobile_debug') && isMobile())
|
||||
eruda.init();
|
||||
}).catch((err: ApiError) => {
|
||||
if(err.code !== 3 && err.text)
|
||||
@@ -99,8 +99,8 @@ class App extends React.Component<Props, State> {
|
||||
});
|
||||
|
||||
getVersion().then(res => {
|
||||
if(version !== res.version && !/(dev\..+)/.test(window.location.href)) {
|
||||
console.debug(`Current back-end version: ${res.version}. Clearing cache`);
|
||||
if(build != res.build && !/(dev\..+)/.test(window.location.href)) {
|
||||
console.debug(`Current back-end version: ${res.version} (${res.build}). Clearing cache...`);
|
||||
|
||||
caches.keys().then(names => {
|
||||
names.forEach(name => caches.delete(name).catch(console.error))
|
||||
|
||||
@@ -212,7 +212,7 @@ class Novel extends React.Component<Props, State> {
|
||||
|
||||
{hasPermission(currentUser, 'novel') && (
|
||||
<div className="Novel__Main_Status_Mark">
|
||||
<div className="Novel__Main_Status_Mark_Element">Изменить</div>
|
||||
<Link to={`/kawaii__neko/novels/${novel.id}`} className="Novel__Main_Status_Mark_Element">Изменить</Link>
|
||||
<div className="Novel__Main_Status_Mark_Element" onClick={() => setModal(<ConfirmModal onConfirm={() => console.log(123)}/>)}>Удалить</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -62,6 +62,7 @@ class UserNovels extends React.Component<Props, State> {
|
||||
|
||||
const planned = novels.filter(n => n.status === 'planned');
|
||||
const completed = novels.filter(n => n.status === 'completed');
|
||||
const in_progress = novels.filter(n => n.status === 'in_progress');
|
||||
|
||||
return (
|
||||
<div className='UserNovels'>
|
||||
@@ -77,8 +78,7 @@ class UserNovels extends React.Component<Props, State> {
|
||||
<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}/>)}
|
||||
{planned.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id + 1} viewType={viewType}/>)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
@@ -87,8 +87,16 @@ class UserNovels extends React.Component<Props, State> {
|
||||
Пройденные
|
||||
</div>
|
||||
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||
{completed.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id + 1}
|
||||
viewType={viewType}/>)}
|
||||
{completed.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id + 1} viewType={viewType}/>)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{in_progress.length > 0 && <div className={'UserNovels__List'}>
|
||||
<div className="UserNovels__List_Title">
|
||||
Прохожу
|
||||
</div>
|
||||
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||
{in_progress.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id + 1} viewType={viewType}/>)}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
import { createStore } from "redux";
|
||||
import { composeWithDevTools } from "redux-devtools-extension";
|
||||
import { devToolsEnhancer } from "redux-devtools-extension";
|
||||
import reducers from "./ducks/index";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
const composeEnhancers = composeWithDevTools({ trace: true });
|
||||
const composeEnhancers = devToolsEnhancer({ trace: true });
|
||||
const sentryReduxEnhancer = Sentry.createReduxEnhancer({});
|
||||
|
||||
const store = process.env.NODE_ENV === 'development'
|
||||
? createStore(reducers, composeEnhancers())
|
||||
? createStore(reducers, composeEnhancers)
|
||||
: createStore(reducers, sentryReduxEnhancer);
|
||||
export default store;
|
||||
|
||||
Vendored
+1
@@ -22,6 +22,7 @@ type ApiResponse<T> = {
|
||||
|
||||
type VersionResponse = {
|
||||
version: string
|
||||
build: number
|
||||
}
|
||||
|
||||
type CommentsResponse = {
|
||||
|
||||
Vendored
+1
@@ -31,6 +31,7 @@ type NovelType = {
|
||||
duration: number
|
||||
platforms: string
|
||||
links: NovelLinkType[]
|
||||
genres: GenreType[]
|
||||
}
|
||||
|
||||
type CharacterType = {
|
||||
|
||||
@@ -8,7 +8,7 @@ const Button = (restProps: ButtonHTMLAttributes<HTMLElement>) => {
|
||||
<button className={classNames('Button', className)} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
|
||||
@@ -95,7 +95,7 @@ class NotificationMessage extends React.Component<Props, State> {
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<div className="Progress" style={{ width: `${time / (5000 || hideTime) * 100}%` }}/>
|
||||
<div className={'Progress'} style={{ width: `${time / (5000 || hideTime) * 100}%` }}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,58 +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
|
||||
@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
|
||||
|
||||
|
||||
.NovelItem__Image
|
||||
border-radius: $borderRadius $borderRadius 0 0
|
||||
background-size: cover
|
||||
background-position: center
|
||||
width: 100%
|
||||
height: 130px
|
||||
|
||||
.NovelItem__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
|
||||
|
||||
.NovelItem__Status
|
||||
font-weight: 300
|
||||
display: flex
|
||||
|
||||
@@ -18,21 +18,22 @@ type Props = {
|
||||
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'
|
||||
<div className={'NovelItem__Image'}
|
||||
style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
|
||||
<div className="NovelItem__Title">
|
||||
<div className={'NovelItem__Title'}>
|
||||
{localized_name || original_name}
|
||||
</div>
|
||||
{status && <div className="NovelItem__Status">
|
||||
{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">
|
||||
<div className={'NovelItem__Title'}>
|
||||
{localized_name || original_name}
|
||||
</div>
|
||||
{status && <div className="NovelItem__Status">
|
||||
{status && <div className={'NovelItem__Status'}>
|
||||
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
|
||||
</div>}
|
||||
</>)}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.Select
|
||||
overflow: auto
|
||||
font-size: 1rem
|
||||
background: var(--main-bg-color)
|
||||
color: white
|
||||
border: none
|
||||
|
||||
option
|
||||
background: var(--sec-bg-color)
|
||||
|
||||
&:checked
|
||||
background: var(--nav-bg-color)
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import './Select.sass';
|
||||
import classNames from "../../classNames";
|
||||
|
||||
type Props = React.DetailedHTMLProps<React.SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>
|
||||
|
||||
const Select = ({ children, className, ...props }: Props) => (
|
||||
<select {...props} className={classNames('Select', className)}>
|
||||
{children}
|
||||
</select>
|
||||
);
|
||||
|
||||
export default Select;
|
||||
@@ -8,6 +8,6 @@ const TabItem = ({ children, selected, ...props }: Props) => (
|
||||
<div className={selected ? 'TabItem Selected' : 'TabItem'} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export default TabItem;
|
||||
|
||||
@@ -5,6 +5,6 @@ const Tabs = ({ children }: { children: React.ReactNode[] }) => (
|
||||
<div className={'Tabs'}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export default Tabs;
|
||||
|
||||
@@ -2,12 +2,12 @@ import React 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} />
|
||||
)
|
||||
})
|
||||
const TextField = (restProps: React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, ref: React.Ref<HTMLTextAreaElement>) => {
|
||||
const { className, ...props } = restProps;
|
||||
|
||||
export default TextField;
|
||||
return (
|
||||
<textarea className={classNames('TextField', className)} ref={ref} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export default React.forwardRef(TextField);
|
||||
|
||||
@@ -3,6 +3,6 @@ import './Title.sass';
|
||||
|
||||
const Title = ({ children }: any) => (
|
||||
<div className={'Title'}>{children}</div>
|
||||
)
|
||||
);
|
||||
|
||||
export default Title;
|
||||
|
||||
+8
-1
@@ -30,4 +30,11 @@ const getImage: (path: string) => string = (path: string) => {
|
||||
return `${cdnUrl}/${path}`;
|
||||
}
|
||||
|
||||
export { capitalize, getRandomInt, hasPermission, hasAccessToAdminPanel, generateClassName, getImage };
|
||||
const isMobile = () => {
|
||||
const ua = window.navigator.userAgent;
|
||||
|
||||
return ua.indexOf('Android') !== -1 || ua.indexOf('iPhone') !== -1 || ua.indexOf('iPad') !== -1;
|
||||
|
||||
}
|
||||
|
||||
export { capitalize, getRandomInt, hasPermission, hasAccessToAdminPanel, generateClassName, getImage, isMobile };
|
||||
|
||||
@@ -2113,6 +2113,17 @@
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/tracing@6.2.0":
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.2.0.tgz#76c17e5dd3f1e61c8a4e3bd090f904a63d674765"
|
||||
integrity sha512-pzgM1dePPJysVnzaFCMp+BKtjM5q46HZeyShiR+KcQYvneD3fmUPJigDkkcsB2DcrY3mFvDcswjoqxaTIW7ZBQ==
|
||||
dependencies:
|
||||
"@sentry/hub" "6.2.0"
|
||||
"@sentry/minimal" "6.2.0"
|
||||
"@sentry/types" "6.2.0"
|
||||
"@sentry/utils" "6.2.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/types@6.2.0":
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.2.0.tgz#ca020ff42913c6b9f88a9d0c375b5ee3965a2590"
|
||||
|
||||
Reference in New Issue
Block a user