From 6389ee8e558efa31b26c6aaef16918990b63a042 Mon Sep 17 00:00:00 2001 From: Nix13 Date: Tue, 23 Feb 2021 01:10:19 +0300 Subject: [PATCH] admin panel; footer module; select --- package.json | 1 + src/api/api.ts | 5 +- src/api/novels.ts | 3 +- .../{Footer.sass => Footer.module.sass} | 5 +- src/components/Footer/Footer.tsx | 38 +++-- src/pages/AdminPanel/AdminPanel.tsx | 11 +- src/pages/AdminPanel/Main/APMain.tsx | 39 +++++ src/pages/AdminPanel/Novels/APModifyNovel.tsx | 142 ++++++++++++++++++ src/pages/AdminPanel/Novels/APNovels.sass | 10 ++ src/pages/AdminPanel/Novels/APNovels.tsx | 43 +++++- src/pages/App/App.tsx | 10 +- src/pages/Novel/Novel.tsx | 2 +- src/pages/User/Novels/UserNovels.tsx | 16 +- src/store/store.ts | 6 +- src/types/api.d.ts | 1 + src/types/types.d.ts | 1 + src/ui/Button/Button.tsx | 2 +- src/ui/Notifications/NotificationMessage.tsx | 2 +- src/ui/NovelItem/NovelItem.sass | 116 +++++++------- src/ui/NovelItem/NovelItem.tsx | 11 +- src/ui/Select/Select.sass | 12 ++ src/ui/Select/Select.tsx | 13 ++ src/ui/Tabs/TabItem.tsx | 2 +- src/ui/Tabs/Tabs.tsx | 2 +- src/ui/TextField/TextField.tsx | 16 +- src/ui/Title/Title.tsx | 2 +- src/utils.ts | 9 +- yarn.lock | 11 ++ 28 files changed, 413 insertions(+), 118 deletions(-) rename src/components/Footer/{Footer.sass => Footer.module.sass} (92%) create mode 100644 src/pages/AdminPanel/Main/APMain.tsx create mode 100644 src/pages/AdminPanel/Novels/APModifyNovel.tsx create mode 100644 src/pages/AdminPanel/Novels/APNovels.sass create mode 100644 src/ui/Select/Select.sass create mode 100644 src/ui/Select/Select.tsx diff --git a/package.json b/package.json index 654b95c..ad225df 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/api.ts b/src/api/api.ts index 49d21e3..1c639a3 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -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('version'); -export const version = '1.2'; +export const version = '1.2.2'; +export const build = 121; export { get, post, put, del, TranslateStatus, TranslatePlatform, TranslateExitStatus }; diff --git a/src/api/novels.ts b/src/api/novels.ts index 8bf2223..6f4a40b 100644 --- a/src/api/novels.ts +++ b/src/api/novels.ts @@ -5,10 +5,11 @@ export const fetchNovels = (orderBy?: string) => get(orderBy ? `nov export const createNovel = (novel: NovelType) => post('novels', { ...novel }); export const fetchNovel = (id: number) => get(`novels/${id}`); -export const updateNovel = (id: number, novel: NovelType) => put(`novels/${id}`, { ...novel }); +export const updateNovel = (novel: NovelType) => put(`novels/${novel.id}`, novel); export const fetchNovelCharacters = (id: number) => get(`novels/${id}/characters`); +export const fetchGenres = () => get('genres'); export const fetchNovelGenres = (id: number) => get(`novels/${id}/genres`); export const fetchNovelComments = (id: number, offset = 0, count = 5) => get(`novels/${id}/comments?offset=${offset}&count=${count}`); diff --git a/src/components/Footer/Footer.sass b/src/components/Footer/Footer.module.sass similarity index 92% rename from src/components/Footer/Footer.sass rename to src/components/Footer/Footer.module.sass index 3e170ac..937b789 100644 --- a/src/components/Footer/Footer.sass +++ b/src/components/Footer/Footer.module.sass @@ -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 \ No newline at end of file + text-align: center diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index 76d111e..cd1ba8f 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -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({ version: '', build: 0 }); + useMemo(() => { + getVersion().then(setVersion); + }, []); + return ( -
-
-
-
Команда
+
+
+
+
Команда
Nix13 RonFall
-
-
Соцсети
+
+
Соцсети
-
-
- Unmei © 2019-2021 +
+
Unmei © 2019-2021
+
+ Frontend version: {version} ({build}); + Backend version: {backend.version} ({backend.build}) +
); diff --git a/src/pages/AdminPanel/AdminPanel.tsx b/src/pages/AdminPanel/AdminPanel.tsx index b04ed74..7def1a1 100644 --- a/src/pages/AdminPanel/AdminPanel.tsx +++ b/src/pages/AdminPanel/AdminPanel.tsx @@ -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 { render() { @@ -26,8 +28,7 @@ class AdminPanel extends React.Component { return (
-
Админ панель (в разработке) -
+
Админ панель
Главная Пользователи @@ -36,13 +37,15 @@ class AdminPanel extends React.Component {
+ + - + diff --git a/src/pages/AdminPanel/Main/APMain.tsx b/src/pages/AdminPanel/Main/APMain.tsx new file mode 100644 index 0000000..5cc86d2 --- /dev/null +++ b/src/pages/AdminPanel/Main/APMain.tsx @@ -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 { + state: State = { + backend: null + } + + componentDidMount() { + getVersion().then(backend => this.setState({ backend })); + } + + render() { + const { backend } = this.state; + + if(!backend) return ; + return ( + +
+ Текущая версия сайта: {version} (билд: {build}) +
+
+ Текущая версия API: {backend.version} (билд: {backend.build}) +
+
+ Совпадение версий сборок: {backend.build == build ? 'да' : 'нет'} +
+
+ ); + } +} + +export default APMain; diff --git a/src/pages/AdminPanel/Novels/APModifyNovel.tsx b/src/pages/AdminPanel/Novels/APModifyNovel.tsx new file mode 100644 index 0000000..b743f37 --- /dev/null +++ b/src/pages/AdminPanel/Novels/APModifyNovel.tsx @@ -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 { + 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) => { + 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) => { + const { novel } = this.state; + if(!novel) return; + + novel.localized_name = e.target.value; + this.setState({ novel }); + } + + changeGenres = (e: ChangeEvent) => { + 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 ; + return ( + +
+ + + + { + novel.description = e.target.value; + this.setState({ novel }); + }}/> + + + + + +
+ + +
+ +
+ ); + } +} + +export default connect(null, + dispatch => ({ + setPopout: (modal: React.ReactNode | null) => dispatch(setModal(modal)), + addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification)) + }) +)(APModifyNovel); diff --git a/src/pages/AdminPanel/Novels/APNovels.sass b/src/pages/AdminPanel/Novels/APNovels.sass new file mode 100644 index 0000000..f34b5dd --- /dev/null +++ b/src/pages/AdminPanel/Novels/APNovels.sass @@ -0,0 +1,10 @@ +.APNovel + display: flex + flex-direction: column + + &_Input, &_Button, &_TextField + font-size: 1.2rem + margin: .5rem 0 + + &_TextField + resize: vertical diff --git a/src/pages/AdminPanel/Novels/APNovels.tsx b/src/pages/AdminPanel/Novels/APNovels.tsx index 4e41769..1f57ab6 100644 --- a/src/pages/AdminPanel/Novels/APNovels.tsx +++ b/src/pages/AdminPanel/Novels/APNovels.tsx @@ -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 { - render() { - return ( -
Novels
- ) + state: State = { + novels: [] } + + componentDidMount() { + fetchNovels().then(novels => this.setState({ novels })); + } + + render() { + const { match: { path } } = this.props; + const { novels } = this.state; + + return ( + + + + + {novels.sort((a, b) => a.id-b.id).map(novel => ( + +
+
{novel.id}
+
{novel.original_name}
+
+ + ))} +
+ ) + }; } export default connect( diff --git a/src/pages/App/App.tsx b/src/pages/App/App.tsx index f50f3c8..670bb97 100644 --- a/src/pages/App/App.tsx +++ b/src/pages/App/App.tsx @@ -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 { 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 { }); 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)) diff --git a/src/pages/Novel/Novel.tsx b/src/pages/Novel/Novel.tsx index 67e8e45..0bf4e04 100644 --- a/src/pages/Novel/Novel.tsx +++ b/src/pages/Novel/Novel.tsx @@ -212,7 +212,7 @@ class Novel extends React.Component { {hasPermission(currentUser, 'novel') && (
-
Изменить
+ Изменить
setModal( console.log(123)}/>)}>Удалить
)} diff --git a/src/pages/User/Novels/UserNovels.tsx b/src/pages/User/Novels/UserNovels.tsx index 8bbbc32..27d73d8 100644 --- a/src/pages/User/Novels/UserNovels.tsx +++ b/src/pages/User/Novels/UserNovels.tsx @@ -62,6 +62,7 @@ class UserNovels extends React.Component { 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 (
@@ -77,8 +78,7 @@ class UserNovels extends React.Component {
- {planned.map((novel, id) => )} + {planned.map((novel, id) => )}
} @@ -87,8 +87,16 @@ class UserNovels extends React.Component { Пройденные
- {completed.map((novel, id) => )} + {completed.map((novel, id) => )} +
+
} + + {in_progress.length > 0 &&
+
+ Прохожу +
+
+ {in_progress.map((novel, id) => )}
}
diff --git a/src/store/store.ts b/src/store/store.ts index ec353be..b57ad1e 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -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; diff --git a/src/types/api.d.ts b/src/types/api.d.ts index ceb11ad..025d5db 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -22,6 +22,7 @@ type ApiResponse = { type VersionResponse = { version: string + build: number } type CommentsResponse = { diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 2f3d576..b1aff95 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -31,6 +31,7 @@ type NovelType = { duration: number platforms: string links: NovelLinkType[] + genres: GenreType[] } type CharacterType = { diff --git a/src/ui/Button/Button.tsx b/src/ui/Button/Button.tsx index d8cc318..7aba068 100644 --- a/src/ui/Button/Button.tsx +++ b/src/ui/Button/Button.tsx @@ -8,7 +8,7 @@ const Button = (restProps: ButtonHTMLAttributes) => { - ) + ); } export default Button; diff --git a/src/ui/Notifications/NotificationMessage.tsx b/src/ui/Notifications/NotificationMessage.tsx index a7da369..b1f4694 100644 --- a/src/ui/Notifications/NotificationMessage.tsx +++ b/src/ui/Notifications/NotificationMessage.tsx @@ -95,7 +95,7 @@ class NotificationMessage extends React.Component { {...props} > {children} -
+
); } diff --git a/src/ui/NovelItem/NovelItem.sass b/src/ui/NovelItem/NovelItem.sass index 51c4c78..60ae62c 100644 --- a/src/ui/NovelItem/NovelItem.sass +++ b/src/ui/NovelItem/NovelItem.sass @@ -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 diff --git a/src/ui/NovelItem/NovelItem.tsx b/src/ui/NovelItem/NovelItem.tsx index 5b980b6..0f7210d 100644 --- a/src/ui/NovelItem/NovelItem.tsx +++ b/src/ui/NovelItem/NovelItem.tsx @@ -18,21 +18,22 @@ type Props = { const NovelItem = ({ id, original_name, localized_name, image, status, mark, viewType, novelId }: Props) => ( {viewType === 'grid' && (<> -
-
+
{localized_name || original_name}
- {status &&
+ {status &&
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
} )} + {viewType === 'table' && (<>
{novelId || ''}
-
+
{localized_name || original_name}
- {status &&
+ {status &&
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
} )} diff --git a/src/ui/Select/Select.sass b/src/ui/Select/Select.sass new file mode 100644 index 0000000..58334dd --- /dev/null +++ b/src/ui/Select/Select.sass @@ -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) diff --git a/src/ui/Select/Select.tsx b/src/ui/Select/Select.tsx new file mode 100644 index 0000000..f7b24ff --- /dev/null +++ b/src/ui/Select/Select.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import './Select.sass'; +import classNames from "../../classNames"; + +type Props = React.DetailedHTMLProps, HTMLSelectElement> + +const Select = ({ children, className, ...props }: Props) => ( + +); + +export default Select; diff --git a/src/ui/Tabs/TabItem.tsx b/src/ui/Tabs/TabItem.tsx index 8ef4635..abaee70 100644 --- a/src/ui/Tabs/TabItem.tsx +++ b/src/ui/Tabs/TabItem.tsx @@ -8,6 +8,6 @@ const TabItem = ({ children, selected, ...props }: Props) => (
{children}
-) +); export default TabItem; diff --git a/src/ui/Tabs/Tabs.tsx b/src/ui/Tabs/Tabs.tsx index b5e9eae..73b9e74 100644 --- a/src/ui/Tabs/Tabs.tsx +++ b/src/ui/Tabs/Tabs.tsx @@ -5,6 +5,6 @@ const Tabs = ({ children }: { children: React.ReactNode[] }) => (
{children}
-) +); export default Tabs; diff --git a/src/ui/TextField/TextField.tsx b/src/ui/TextField/TextField.tsx index bff6a64..164e9fd 100644 --- a/src/ui/TextField/TextField.tsx +++ b/src/ui/TextField/TextField.tsx @@ -2,12 +2,12 @@ import React from "react"; import classNames from "../../classNames"; import './TextField.sass'; -const TextField = React.forwardRef( - (restProps: React.DetailedHTMLProps, HTMLTextAreaElement>, ref: React.Ref) => { - const { className, ...props } = restProps; - return ( -