initial commit
This commit is contained in:
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
*.lock
|
||||
|
||||
# Dependency directories
|
||||
package-lock.json
|
||||
/node_modules
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Prod Bundle
|
||||
/build
|
||||
|
||||
.vscode
|
||||
.idea
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Ivan Salvatore
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
93
README.md
Normal file
93
README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# VK Mini App Boilerplate
|
||||
**Стартовый кит для создания сервиса на платформе VK Mini Apps.**
|
||||
|
||||
Чтобы понимать, что здесь происходит вы должны знать что такое VK Mini Apps, ReactJS и React Redux.
|
||||
|
||||
[Документация по VK Mini Apps](https://vk.com/dev/vk_apps_docs)
|
||||
|
||||
[Документация по ReactJS](https://ru.reactjs.org/docs/getting-started.html)
|
||||
|
||||
[Документация по React Redux](https://rajdee.gitbooks.io/redux-in-russian/content/)
|
||||
|
||||
## Установка:
|
||||
`git clone git://github.com/iSa1vatore/vk-mini-app-boilerplate.git <folder name>`
|
||||
|
||||
Перейдите в созданную папку и выполните команды: `npm install` и `npm start`, последняя запустит сервер для разработки на `localhost:10888`
|
||||
|
||||
Отлично, теперь перед нами демонстративное приложение, можно править код и все изменения сразу будут видны на нашем сервере при помощи "Hot Reloading".
|
||||
|
||||
## KIT:
|
||||
#### Что реализовано:
|
||||
- Поддержка темы нативного клиента
|
||||
- Поддержка iOS swipe back для панелей
|
||||
- Обработка хардверной кнопки "назад" для Android
|
||||
- Сохранение позиции скролла для панелей и элементов
|
||||
- Scroll To Top при нажатии на иконку в Epic`e
|
||||
- Получение токена пользователя
|
||||
- Запросы к API ВКонтакте
|
||||
- Сохранение данных в форме при смене панели
|
||||
- Роутер
|
||||
|
||||
#### Роутер:
|
||||
Действия которые роутер может выполнить:
|
||||
- `setStory(story, initial_panel)` - Устанавливает активную Story у Epic'a, View и активную панель.
|
||||
- `setPage(view, panel)` - Устанавливает активный View и Panel
|
||||
- `goBack(from)` - Совершает действие назад, будь то закрытие модального окна, переход на прошлую панель, закрытие попапа и т.п;
|
||||
- `openPopout(popout)` - Открывает поппап.
|
||||
- `closePopout()` - Закрывает поппап.
|
||||
- `openModal(id)` - Открывает модальную страницу по её ID.
|
||||
- `closeModal()` - Закрывает модальную страницу или открывает прошлую страницу.
|
||||
|
||||
Примеры того как это все работает смотрите в исходниках ¯\_(ツ)_/¯, там все просто.
|
||||
|
||||
#### Сохранение позиции скролла:
|
||||
Для сохранения позиции горизонтального скоролла нужно:
|
||||
|
||||
- Указать ID для элемента HorizontalScroll
|
||||
```javascript
|
||||
<HorizontalScroll id="EXAMPLE_TABS_LIST">
|
||||
...
|
||||
</HorizontalScroll>
|
||||
```
|
||||
- Сохранить позицию при демонтировании
|
||||
```javascript
|
||||
componentWillUnmount() {
|
||||
const {setScrollPositionByID} = this.props;
|
||||
|
||||
setScrollPositionByID("EXAMPLE_TABS_LIST");
|
||||
}
|
||||
```
|
||||
- Восстановить позицию при монтировании
|
||||
```javascript
|
||||
componentDidMount() {
|
||||
restoreScrollPosition();
|
||||
}
|
||||
```
|
||||
Пример находится в файле: [`/src/js/panels/more/base.js`](https://github.com/iSa1vatore/vk-mini-app-boilerplate/blob/master/src/js/panels/more/base.js)
|
||||
#### Важно:
|
||||
В файле index.js на 24 стороке указывается стартовая панель приложения:
|
||||
```javascript
|
||||
store.dispatch(setStory('home', 'base'));
|
||||
```
|
||||
В данном случае это значит, что приложение запустится с:
|
||||
|
||||
`activeStory: home`
|
||||
|
||||
`activeView: home`
|
||||
|
||||
`activePanel: base`
|
||||
|
||||
Как вы поняли значение ID у Root и стартового View должны совпадать.
|
||||
|
||||
В проекте есть 2 файла: "App" и "AppWithoutEpic", первый идет с навигационной панелью Epic, второй без, он подойдет для простых приложений.
|
||||
```javascript
|
||||
import App from './App';
|
||||
```
|
||||
По умолчанию для примера выбран вариант с Epic навигацией.
|
||||
|
||||
В файле по пути [`/src/js/services/VK.js`](https://github.com/iSa1vatore/vk-mini-app-boilerplate/blob/master/src/js/services/VK.js) нужно заменить значение константы `APP_ID` на ID вашего приложения
|
||||
|
||||
|
||||
Демо: [vk.com/app6984089](https://vk.com/app6984089)
|
||||
|
||||
Мой VK (вопросы, предложения): [Ivan Salvatore](https://vk.com/s9008)
|
||||
39
package.json
Normal file
39
package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "vkminiappboilerplate",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": " cross-env PORT=10888 react-scripts start",
|
||||
"build": "react-scripts build"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"cross-env": "^5.2.1",
|
||||
"react-hot-loader": "^4.12.19",
|
||||
"react-scripts": "^2.1.8",
|
||||
"redux-devtools-extension": "^2.13.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vkontakte/icons": "^1.16.1",
|
||||
"@vkontakte/vk-connect": "^1.8.7",
|
||||
"@vkontakte/vkui": "^3.2.0",
|
||||
"babel-eslint": "^9.0.0",
|
||||
"chalk": "^2.4.2",
|
||||
"core-js": "^2.6.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^7.1.3",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
13
public/index.html
Normal file
13
public/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<title>VK Mini App Boilerplate by iSa1vatore</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
154
src/App.js
Normal file
154
src/App.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {goBack, closeModal, setStory} from "./js/store/router/actions";
|
||||
import {getActivePanel} from "./js/services/_functions";
|
||||
import * as VK from './js/services/VK';
|
||||
|
||||
import {Epic, View, Root, Tabbar, ModalRoot, TabbarItem, ConfigProvider} from "@vkontakte/vkui";
|
||||
|
||||
import Icon28Newsfeed from '@vkontakte/icons/dist/28/newsfeed';
|
||||
import Icon28More from '@vkontakte/icons/dist/28/more';
|
||||
|
||||
import HomePanelBase from './js/panels/home/base';
|
||||
import HomePanelGroups from './js/panels/home/groups';
|
||||
|
||||
import MorePanelBase from './js/panels/more/base';
|
||||
import MorePanelExample from './js/panels/more/example';
|
||||
|
||||
import HomeBotsListModal from './js/components/modals/HomeBotsListModal';
|
||||
import HomeBotInfoModal from './js/components/modals/HomeBotInfoModal';
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.lastAndroidBackAction = 0;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {goBack, dispatch} = this.props;
|
||||
|
||||
dispatch(VK.initApp());
|
||||
|
||||
window.onpopstate = () => {
|
||||
let timeNow = +new Date();
|
||||
|
||||
if (timeNow - this.lastAndroidBackAction > 500) {
|
||||
this.lastAndroidBackAction = timeNow;
|
||||
|
||||
goBack();
|
||||
} else {
|
||||
window.history.pushState(null, null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
const {activeView, activeStory, activePanel, scrollPosition} = this.props;
|
||||
|
||||
if (
|
||||
prevProps.activeView !== activeView ||
|
||||
prevProps.activePanel !== activePanel ||
|
||||
prevProps.activeStory !== activeStory
|
||||
) {
|
||||
let pageScrollPosition = scrollPosition[activeStory + "_" + activeView + "_" + activePanel] || 0;
|
||||
|
||||
window.scroll(0, pageScrollPosition);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {goBack, setStory, closeModal, popouts, activeView, activeStory, activeModals, panelsHistory, colorScheme} = this.props;
|
||||
|
||||
let history = (panelsHistory[activeView] === undefined) ? [activeView] : panelsHistory[activeView];
|
||||
let popout = (popouts[activeView] === undefined) ? null : popouts[activeView];
|
||||
let activeModal = (activeModals[activeView] === undefined) ? null : activeModals[activeView];
|
||||
|
||||
const homeModals = (
|
||||
<ModalRoot activeModal={activeModal}>
|
||||
<HomeBotsListModal
|
||||
id="MODAL_PAGE_BOTS_LIST"
|
||||
onClose={() => closeModal()}
|
||||
/>
|
||||
<HomeBotInfoModal
|
||||
id="MODAL_PAGE_BOT_INFO"
|
||||
onClose={() => closeModal()}
|
||||
/>
|
||||
</ModalRoot>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigProvider isWebView={true} scheme={colorScheme}>
|
||||
<Epic activeStory={activeStory} tabbar={
|
||||
<Tabbar>
|
||||
<TabbarItem
|
||||
onClick={() => setStory('home', 'base')}
|
||||
selected={activeStory === 'home'}
|
||||
><Icon28Newsfeed/></TabbarItem>
|
||||
<TabbarItem
|
||||
onClick={() => setStory('more', 'callmodal')}
|
||||
selected={activeStory === 'more'}
|
||||
><Icon28More/></TabbarItem>
|
||||
</Tabbar>
|
||||
}>
|
||||
<Root id="home" activeView={activeView} popout={popout}>
|
||||
<View
|
||||
id="home"
|
||||
modal={homeModals}
|
||||
activePanel={getActivePanel("home")}
|
||||
history={history}
|
||||
onSwipeBack={() => goBack()}
|
||||
>
|
||||
<HomePanelBase id="base" withoutEpic={false}/>
|
||||
<HomePanelGroups id="groups"/>
|
||||
</View>
|
||||
</Root>
|
||||
<Root id="more" activeView={activeView} popout={popout}>
|
||||
<View
|
||||
id="more"
|
||||
modal={homeModals}
|
||||
activePanel={getActivePanel("more")}
|
||||
history={history}
|
||||
onSwipeBack={() => goBack()}
|
||||
>
|
||||
<MorePanelBase id="callmodal"/>
|
||||
</View>
|
||||
<View
|
||||
id="modal"
|
||||
modal={homeModals}
|
||||
activePanel={getActivePanel("modal")}
|
||||
history={history}
|
||||
onSwipeBack={() => goBack()}
|
||||
>
|
||||
<MorePanelExample id="filters"/>
|
||||
</View>
|
||||
</Root>
|
||||
</Epic>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
activeView: state.router.activeView,
|
||||
activeStory: state.router.activeStory,
|
||||
panelsHistory: state.router.panelsHistory,
|
||||
activeModals: state.router.activeModals,
|
||||
popouts: state.router.popouts,
|
||||
scrollPosition: state.router.scrollPosition,
|
||||
|
||||
colorScheme: state.vkui.colorScheme
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
dispatch,
|
||||
...bindActionCreators({setStory, goBack, closeModal}, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||
126
src/AppWithoutEpic.js
Normal file
126
src/AppWithoutEpic.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {goBack, closeModal} from "./js/store/router/actions";
|
||||
import {getActivePanel} from "./js/services/_functions";
|
||||
import * as VK from './js/services/VK';
|
||||
|
||||
import {View, Root, ModalRoot, ConfigProvider} from "@vkontakte/vkui";
|
||||
|
||||
import HomePanelProfile from './js/panels/home/base';
|
||||
import HomePanelGroups from './js/panels/home/groups';
|
||||
|
||||
import HomeBotsListModal from './js/components/modals/HomeBotsListModal';
|
||||
import HomeBotInfoModal from './js/components/modals/HomeBotInfoModal';
|
||||
|
||||
import MorePanelExample from './js/panels/more/example';
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.lastAndroidBackAction = 0;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const {goBack, dispatch} = this.props;
|
||||
|
||||
dispatch(VK.initApp());
|
||||
|
||||
window.onpopstate = () => {
|
||||
let timeNow = +new Date();
|
||||
|
||||
if (timeNow - this.lastAndroidBackAction > 500) {
|
||||
this.lastAndroidBackAction = timeNow;
|
||||
|
||||
goBack();
|
||||
} else {
|
||||
window.history.pushState(null, null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
const {activeView, activeStory, activePanel, scrollPosition} = this.props;
|
||||
|
||||
if (
|
||||
prevProps.activeView !== activeView ||
|
||||
prevProps.activePanel !== activePanel ||
|
||||
prevProps.activeStory !== activeStory
|
||||
) {
|
||||
let pageScrollPosition = scrollPosition[activeStory + "_" + activeView + "_" + activePanel] || 0;
|
||||
|
||||
window.scroll(0, pageScrollPosition);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {goBack, closeModal, popouts, activeView, activeModals, panelsHistory, colorScheme} = this.props;
|
||||
|
||||
let history = (panelsHistory[activeView] === undefined) ? [activeView] : panelsHistory[activeView];
|
||||
let popout = (popouts[activeView] === undefined) ? null : popouts[activeView];
|
||||
let activeModal = (activeModals[activeView] === undefined) ? null : activeModals[activeView];
|
||||
|
||||
const homeModals = (
|
||||
<ModalRoot activeModal={activeModal}>
|
||||
<HomeBotsListModal
|
||||
id="MODAL_PAGE_BOTS_LIST"
|
||||
onClose={() => closeModal()}
|
||||
/>
|
||||
<HomeBotInfoModal
|
||||
id="MODAL_PAGE_BOT_INFO"
|
||||
onClose={() => closeModal()}
|
||||
/>
|
||||
</ModalRoot>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigProvider isWebView={true} scheme={colorScheme}>
|
||||
<Root activeView={activeView} popout={popout}>
|
||||
<View
|
||||
id="home"
|
||||
modal={homeModals}
|
||||
activePanel={getActivePanel("home")}
|
||||
history={history}
|
||||
onSwipeBack={() => goBack()}
|
||||
>
|
||||
<HomePanelProfile id="base" withoutEpic={true}/>
|
||||
<HomePanelGroups id="groups"/>
|
||||
</View>
|
||||
<View
|
||||
id="modal"
|
||||
modal={homeModals}
|
||||
activePanel={getActivePanel("modal")}
|
||||
history={history}
|
||||
onSwipeBack={() => goBack()}
|
||||
>
|
||||
<MorePanelExample id="filters"/>
|
||||
</View>
|
||||
</Root>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
activeView: state.router.activeView,
|
||||
activeStory: state.router.activeStory,
|
||||
panelsHistory: state.router.panelsHistory,
|
||||
activeModals: state.router.activeModals,
|
||||
popouts: state.router.popouts,
|
||||
scrollPosition: state.router.scrollPosition,
|
||||
|
||||
colorScheme: state.vkui.colorScheme
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
dispatch,
|
||||
...bindActionCreators({goBack, closeModal}, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||
23
src/css/main.css
Normal file
23
src/css/main.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.div-center {
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.div-center img {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
.buttons-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.buttons-group button:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
a, div, img, p, button {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
31
src/index.js
Normal file
31
src/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'core-js/es6/map';
|
||||
import 'core-js/es6/set';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {applyMiddleware, createStore} from "redux";
|
||||
import thunk from 'redux-thunk';
|
||||
import {Provider} from 'react-redux';
|
||||
import rootReducer from './js/store/reducers';
|
||||
import {composeWithDevTools} from 'redux-devtools-extension';
|
||||
|
||||
import {setStory} from "./js/store/router/actions";
|
||||
|
||||
import '@vkontakte/vkui/dist/vkui.css';
|
||||
import './css/main.css';
|
||||
|
||||
import App from './App';
|
||||
|
||||
export const store = createStore(rootReducer, composeWithDevTools(
|
||||
applyMiddleware(thunk),
|
||||
));
|
||||
|
||||
store.dispatch(setStory('home', 'base'));
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App/>
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
24
src/js/components/GroupCell.js
Normal file
24
src/js/components/GroupCell.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
|
||||
import {Cell, Avatar} from "@vkontakte/vkui";
|
||||
|
||||
class GroupCell extends PureComponent {
|
||||
|
||||
render() {
|
||||
const {description, photo, name} = this.props;
|
||||
|
||||
let desc = description.length > 0 ? description : "Описание отсутствует";
|
||||
|
||||
return (
|
||||
<Cell
|
||||
description={desc}
|
||||
before={<Avatar size={40} src={photo}/>}
|
||||
>
|
||||
{name}
|
||||
</Cell>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default GroupCell;
|
||||
59
src/js/components/modals/HomeBotInfoModal.js
Normal file
59
src/js/components/modals/HomeBotInfoModal.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {Cell, List, Avatar, InfoRow, ModalPage, ModalPageHeader, PanelHeaderButton, withPlatform, IOS} from "@vkontakte/vkui";
|
||||
|
||||
import Icon24Dismiss from '@vkontakte/icons/dist/24/dismiss';
|
||||
import Icon24Cancel from '@vkontakte/icons/dist/24/cancel';
|
||||
|
||||
class HomeBotsListModal extends React.Component {
|
||||
|
||||
render() {
|
||||
const {id, onClose, platform} = this.props;
|
||||
|
||||
return (
|
||||
<ModalPage
|
||||
id={id}
|
||||
header={
|
||||
<ModalPageHeader
|
||||
left={platform !== IOS &&
|
||||
<PanelHeaderButton onClick={onClose}><Icon24Cancel/></PanelHeaderButton>}
|
||||
right={platform === IOS &&
|
||||
<PanelHeaderButton onClick={onClose}><Icon24Dismiss/></PanelHeaderButton>}
|
||||
>
|
||||
Информация о боте
|
||||
</ModalPageHeader>
|
||||
}
|
||||
onClose={onClose}
|
||||
settlingHeight={80}
|
||||
>
|
||||
<Cell
|
||||
description="Описание"
|
||||
before={<Avatar size={40} src="https://vk.com/images/community_100.png?ava=1"/>}
|
||||
>
|
||||
Название
|
||||
</Cell>
|
||||
<List>
|
||||
<Cell>
|
||||
<InfoRow header="Подписчиков">
|
||||
8800
|
||||
</InfoRow>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<InfoRow header="Записей">
|
||||
555
|
||||
</InfoRow>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<InfoRow header="Рейтинг">
|
||||
3535
|
||||
</InfoRow>
|
||||
</Cell>
|
||||
</List>
|
||||
</ModalPage>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withPlatform(connect()(HomeBotsListModal));
|
||||
74
src/js/components/modals/HomeBotsListModal.js
Normal file
74
src/js/components/modals/HomeBotsListModal.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {openModal} from "../../store/router/actions";
|
||||
|
||||
import {List, Cell, Avatar, ModalPage, ModalPageHeader, PanelHeaderButton, withPlatform, IOS} from "@vkontakte/vkui";
|
||||
|
||||
import Icon24Dismiss from '@vkontakte/icons/dist/24/dismiss';
|
||||
import Icon24Cancel from '@vkontakte/icons/dist/24/cancel';
|
||||
import Icon24Chevron from '@vkontakte/icons/dist/24/chevron';
|
||||
|
||||
const bots = [
|
||||
{
|
||||
name: 'VKS',
|
||||
avatar: 'https://pp.userapi.com/c851520/v851520442/48ce/Sik7V4c58qw.jpg',
|
||||
desc: 'Нет, меня не роняли в детстве'
|
||||
},
|
||||
{
|
||||
name: 'Недобот',
|
||||
avatar: 'https://pp.userapi.com/c854420/v854420431/da51/X8ohw4-4Fk4.jpg',
|
||||
desc: 'Я ни разу не пил кокосовое молоко'
|
||||
},
|
||||
{
|
||||
name: 'Realm of War',
|
||||
avatar: 'https://sun9-72.userapi.com/c853520/v853520591/1f167d/avj_z9yFtQA.jpg?ava=1',
|
||||
desc: 'Ненавижу спойлеры.'
|
||||
},
|
||||
];
|
||||
|
||||
class HomeBotsListModal extends React.Component {
|
||||
|
||||
render() {
|
||||
const {id, onClose, openModal, platform} = this.props;
|
||||
|
||||
return (
|
||||
<ModalPage
|
||||
id={id}
|
||||
header={
|
||||
<ModalPageHeader
|
||||
left={platform !== IOS &&
|
||||
<PanelHeaderButton onClick={onClose}><Icon24Cancel/></PanelHeaderButton>}
|
||||
right={platform === IOS &&
|
||||
<PanelHeaderButton onClick={onClose}><Icon24Dismiss/></PanelHeaderButton>}
|
||||
>
|
||||
/appbots на минималках
|
||||
</ModalPageHeader>
|
||||
}
|
||||
onClose={onClose}
|
||||
settlingHeight={80}
|
||||
>
|
||||
<List>
|
||||
{bots.map((bot, index) => (
|
||||
<Cell
|
||||
key={index}
|
||||
description={bot.desc}
|
||||
before={<Avatar size={40} src={bot.avatar}/>}
|
||||
onClick={() => openModal('MODAL_PAGE_BOT_INFO')}
|
||||
asideContent={<Icon24Chevron fill="#528bcc"/>}
|
||||
>
|
||||
{bot.name}
|
||||
</Cell>
|
||||
))}
|
||||
</List>
|
||||
</ModalPage>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openModal
|
||||
};
|
||||
|
||||
export default withPlatform(connect(null, mapDispatchToProps)(HomeBotsListModal));
|
||||
87
src/js/panels/home/base.js
Normal file
87
src/js/panels/home/base.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { closePopout, goBack, openModal, openPopout, setPage } from '../../store/router/actions';
|
||||
|
||||
import { Div, Panel, Alert, Group, Button, PanelHeader } from "@vkontakte/vkui"
|
||||
|
||||
class HomePanelBase extends React.Component {
|
||||
|
||||
state = {
|
||||
showImg: false
|
||||
};
|
||||
|
||||
showImg = () => {
|
||||
this.setState({ showImg: true });
|
||||
};
|
||||
|
||||
openPopout() {
|
||||
this.props.openPopout(
|
||||
<Alert
|
||||
actions={[{
|
||||
title: 'Нет',
|
||||
autoclose: true,
|
||||
style: 'cancel',
|
||||
}, {
|
||||
title: 'Да',
|
||||
autoclose: true,
|
||||
action: this.showImg
|
||||
}]}
|
||||
onClose={() => this.props.closePopout()}
|
||||
>
|
||||
<h2>Вопрос значит</h2>
|
||||
<p>Вас роняли в детстве?</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { id, setPage, withoutEpic } = this.props;
|
||||
const url = new URL(window.location.href);
|
||||
const payload = async () => {
|
||||
if (url.searchParams.get('payload') != null) return await url.searchParams.get('payload');
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel id={id}>
|
||||
<PanelHeader>Examples</PanelHeader>
|
||||
<Group>
|
||||
{/* <Div>
|
||||
<Button mode="secondary" size="l" stretched={true} onClick={() => setPage('home', 'groups')}>Список моих
|
||||
групп</Button>
|
||||
</Div> */}
|
||||
<Div>
|
||||
<p>
|
||||
Ваш ник: {new URL(window.location.href).searchParams.get('payload') ? new URL(window.location.href).searchParams.get('payload') : ''}
|
||||
</p>
|
||||
</Div>
|
||||
{/* <Div>
|
||||
<p>{params.get('payload')}</p>
|
||||
</Div> */}
|
||||
<Div>
|
||||
<Button mode="secondary" size="l" stretched={true} onClick={() => this.openPopout()}>Открыть алерт</Button>
|
||||
</Div>
|
||||
<Div>
|
||||
<Button mode="secondary" size="l" stretched={true} onClick={() => this.props.openModal("MODAL_PAGE_BOTS_LIST")}>Открыть
|
||||
модальную страницу</Button>
|
||||
</Div>
|
||||
|
||||
</Group>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setPage,
|
||||
goBack,
|
||||
openPopout,
|
||||
closePopout,
|
||||
openModal
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(HomePanelBase);
|
||||
145
src/js/panels/home/groups.js
Normal file
145
src/js/panels/home/groups.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from "redux";
|
||||
|
||||
import {goBack, openPopout, closePopout, openModal} from "../../store/router/actions";
|
||||
import * as VK from '../../services/VK';
|
||||
|
||||
import {renderGroupsList} from '../../services/renderers';
|
||||
|
||||
import {Div, List, Panel, Group, Button, PanelHeader, PanelSpinner, PanelHeaderBack, Header} from "@vkontakte/vkui";
|
||||
|
||||
class HomePanelGroups extends React.Component {
|
||||
|
||||
state = {
|
||||
groups: {
|
||||
admined: [],
|
||||
other: [],
|
||||
},
|
||||
loading: true,
|
||||
errorGetAuthToken: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.accessToken === undefined) {
|
||||
this.getAuthToken();
|
||||
} else {
|
||||
this.getGroupsList();
|
||||
}
|
||||
}
|
||||
|
||||
getAuthToken() {
|
||||
this.props.dispatch(VK.getAuthToken(['groups']));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
if (this.props !== prevProps) {
|
||||
if (this.props.accessToken === null) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorGetAuthToken: true
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
loading: true,
|
||||
errorGetAuthToken: false
|
||||
});
|
||||
|
||||
this.getGroupsList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getGroupsList() {
|
||||
if (localStorage.getItem('userGroupsAdmined') && localStorage.getItem('userGroupsOther')) {
|
||||
this.setState({
|
||||
groups: {
|
||||
admined: JSON.parse(localStorage.getItem('userGroupsAdmined')),
|
||||
other: JSON.parse(localStorage.getItem('userGroupsOther')),
|
||||
},
|
||||
loading: false
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let groups = await VK.groupsGet();
|
||||
console.log(groups);
|
||||
|
||||
let adminedGroups = groups.items.filter(function (item) {
|
||||
return item.is_admin === 1;
|
||||
});
|
||||
|
||||
let otherGroups = groups.items.filter(function (item) {
|
||||
return item.is_admin === 0;
|
||||
});
|
||||
|
||||
localStorage.setItem('userGroupsAdmined', JSON.stringify(adminedGroups));
|
||||
localStorage.setItem('userGroupsOther', JSON.stringify(otherGroups));
|
||||
|
||||
this.setState({
|
||||
groups: {
|
||||
admined: adminedGroups,
|
||||
other: otherGroups
|
||||
},
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {id, goBack} = this.props;
|
||||
|
||||
let adminedGroupsList = renderGroupsList(this.state.groups.admined);
|
||||
let otherGroupsList = renderGroupsList(this.state.groups.other);
|
||||
|
||||
return (
|
||||
<Panel id={id}>
|
||||
<PanelHeader
|
||||
left={<PanelHeaderBack onClick={() => goBack()}/>}
|
||||
>
|
||||
Группы
|
||||
</PanelHeader>
|
||||
{this.state.loading && <PanelSpinner/>}
|
||||
{!this.state.loading && this.state.errorGetAuthToken && <Group>
|
||||
<Div>Возникла ошибка при получении данных, возможно Вы не выдали права на список групп.</Div>
|
||||
<Div>
|
||||
<Button size="l" stretched={true} onClick={() => this.getAuthToken()}>Запросить
|
||||
повторно</Button>
|
||||
</Div>
|
||||
</Group>}
|
||||
{!this.state.loading && !this.state.errorGetAuthToken && adminedGroupsList &&
|
||||
<Group header={
|
||||
<Header mode="secondary">Администрируемые</Header>
|
||||
}>
|
||||
<List>
|
||||
{adminedGroupsList}
|
||||
</List>
|
||||
</Group>}
|
||||
{!this.state.loading && !this.state.errorGetAuthToken && otherGroupsList &&
|
||||
<Group header={
|
||||
<Header mode="secondary">Остальные</Header>
|
||||
}>
|
||||
<List>
|
||||
{otherGroupsList}
|
||||
</List>
|
||||
</Group>}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
dispatch,
|
||||
...bindActionCreators({goBack, openPopout, closePopout, openModal}, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
accessToken: state.vkui.accessToken
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(HomePanelGroups);
|
||||
109
src/js/panels/more/base.js
Normal file
109
src/js/panels/more/base.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {setPage} from "../../store/router/actions";
|
||||
import {setActiveTab, setScrollPositionByID} from "../../store/vk/actions";
|
||||
import {restoreScrollPosition} from "../../services/_functions";
|
||||
|
||||
import {
|
||||
Div,
|
||||
Panel,
|
||||
Group,
|
||||
CellButton,
|
||||
PanelHeader,
|
||||
FixedLayout,
|
||||
HorizontalScroll,
|
||||
TabsItem,
|
||||
Tabs
|
||||
} from "@vkontakte/vkui";
|
||||
|
||||
class HomePanelProfile extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeTab: props.activeTab["EXAMPLE"] || "modal"
|
||||
};
|
||||
}
|
||||
|
||||
setTab(tab) {
|
||||
this.setState({
|
||||
activeTab: tab
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const {setScrollPositionByID, setActiveTab} = this.props;
|
||||
|
||||
setActiveTab("EXAMPLE", this.state.activeTab);
|
||||
setScrollPositionByID("EXAMPLE_TABS_LIST");
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
restoreScrollPosition();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {id, setPage} = this.props;
|
||||
const boxStyle = {marginTop: 56};
|
||||
|
||||
return (
|
||||
<Panel id={id}>
|
||||
<PanelHeader noShadow={true}>Examples 2</PanelHeader>
|
||||
<FixedLayout vertical="top">
|
||||
<Tabs theme="header" mode="default">
|
||||
<HorizontalScroll id="EXAMPLE_TABS_LIST">
|
||||
<TabsItem
|
||||
onClick={() => this.setTab('modal')}
|
||||
selected={this.state.activeTab === 'modal'}
|
||||
>
|
||||
Модальное окно
|
||||
</TabsItem>
|
||||
<TabsItem
|
||||
onClick={() => this.setTab('test')}
|
||||
selected={this.state.activeTab === 'test'}
|
||||
>
|
||||
Для теста
|
||||
</TabsItem>
|
||||
<TabsItem
|
||||
onClick={() => this.setTab('test2')}
|
||||
selected={this.state.activeTab === 'test2'}
|
||||
>
|
||||
Для теста 2
|
||||
</TabsItem>
|
||||
<TabsItem
|
||||
onClick={() => this.setTab('test3')}
|
||||
selected={this.state.activeTab === 'test3'}
|
||||
>
|
||||
Для теста 3
|
||||
</TabsItem>
|
||||
</HorizontalScroll>
|
||||
</Tabs>
|
||||
</FixedLayout>
|
||||
<Group style={boxStyle}>
|
||||
{this.state.activeTab === 'modal' && <CellButton onClick={() => setPage('modal', 'filters')}>
|
||||
Открыть модальное окно
|
||||
</CellButton>}
|
||||
|
||||
{this.state.activeTab !== 'modal' && <Div>{this.state.activeTab}</Div>}
|
||||
</Group>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
activeTab: state.vkui.activeTab,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setPage,
|
||||
setActiveTab,
|
||||
setScrollPositionByID
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(HomePanelProfile);
|
||||
120
src/js/panels/more/example.js
Normal file
120
src/js/panels/more/example.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {goBack} from '../../store/router/actions';
|
||||
import {setFormData} from "../../store/formData/actions";
|
||||
|
||||
import {
|
||||
Div,
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
FormLayout,
|
||||
FormLayoutGroup,
|
||||
Panel,
|
||||
Group,
|
||||
PanelHeader,
|
||||
PanelHeaderBack
|
||||
} from "@vkontakte/vkui";
|
||||
|
||||
class HomePanelProfile extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let defaultInputData = {
|
||||
workplace: '',
|
||||
workposition: '',
|
||||
|
||||
checkboxPhoto: 0,
|
||||
checkboxOnline: 0
|
||||
};
|
||||
|
||||
this.state = {
|
||||
inputData: props.inputData['example_form'] || defaultInputData
|
||||
};
|
||||
|
||||
this.handleInput = (e) => {
|
||||
let value = e.currentTarget.value;
|
||||
|
||||
if (e.currentTarget.type === 'checkbox') {
|
||||
value = e.currentTarget.checked;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
inputData: {
|
||||
...this.state.inputData,
|
||||
[e.currentTarget.name]: value
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
this.clearForm = () => {
|
||||
this.setState({
|
||||
inputData: defaultInputData
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.setFormData('example_form', this.state.inputData);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {id, goBack} = this.props;
|
||||
|
||||
return (
|
||||
<Panel id={id}>
|
||||
<PanelHeader
|
||||
left={<PanelHeaderBack onClick={() => goBack()}/>}
|
||||
>
|
||||
Модальное окно
|
||||
</PanelHeader>
|
||||
<Group>
|
||||
<FormLayout>
|
||||
<FormLayoutGroup top="Работа">
|
||||
<Input value={this.state.inputData.workplace}
|
||||
onChange={this.handleInput}
|
||||
name="workplace"
|
||||
placeholder="Место работы"
|
||||
autoComplete="off"/>
|
||||
<Input value={this.state.inputData.workposition}
|
||||
onChange={this.handleInput}
|
||||
name="workposition"
|
||||
placeholder="Должность"
|
||||
autoComplete="off"/>
|
||||
</FormLayoutGroup>
|
||||
<FormLayoutGroup top="Дополнительно">
|
||||
<Checkbox checked={this.state.inputData.checkboxPhoto}
|
||||
onChange={this.handleInput}
|
||||
name="checkboxPhoto"
|
||||
>С фотографией</Checkbox>
|
||||
<Checkbox checked={this.state.inputData.checkboxOnline}
|
||||
onChange={this.handleInput}
|
||||
name="checkboxOnline"
|
||||
>Сейчас на сайте</Checkbox>
|
||||
</FormLayoutGroup>
|
||||
</FormLayout>
|
||||
<Div className="buttons-group">
|
||||
<Button size="l" stretched={true} onClick={() => goBack()}>Сохранить</Button>
|
||||
<Button size="l" stretched={true} onClick={this.clearForm}>Очистить</Button>
|
||||
</Div>
|
||||
</Group>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
inputData: state.formData.forms,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setFormData,
|
||||
goBack
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(HomePanelProfile);
|
||||
84
src/js/services/VK.js
Normal file
84
src/js/services/VK.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import VKConnect from "@vkontakte/vk-connect";
|
||||
|
||||
import {store} from "../../index";
|
||||
|
||||
import {setColorScheme, setAccessToken} from "../store/vk/actions";
|
||||
|
||||
const APP_ID = 6984089;
|
||||
const API_VERSION = '5.92';
|
||||
|
||||
export const initApp = () => (dispatch) => {
|
||||
const VKConnectCallback = (e) => {
|
||||
if (e.detail.type === 'VKWebAppUpdateConfig') {
|
||||
VKConnect.unsubscribe(VKConnectCallback);
|
||||
|
||||
dispatch(setColorScheme(e.detail.data.scheme));
|
||||
}
|
||||
};
|
||||
|
||||
VKConnect.subscribe(VKConnectCallback);
|
||||
return VKConnect.send('VKWebAppInit', {}).then(data => {
|
||||
return data;
|
||||
}).catch(error => {
|
||||
return error;
|
||||
});
|
||||
};
|
||||
|
||||
export const getAuthToken = (scope) => (dispatch) => {
|
||||
VKConnect.send("VKWebAppGetAuthToken", {
|
||||
"app_id": APP_ID,
|
||||
"scope": scope.join(',')
|
||||
}).then(data => {
|
||||
dispatch(setAccessToken(data.access_token));
|
||||
}).catch(() => {
|
||||
dispatch(setAccessToken(null));
|
||||
});
|
||||
};
|
||||
|
||||
export const closeApp = () => {
|
||||
return VKConnect.send("VKWebAppClose", {
|
||||
"status": "success"
|
||||
}).then(data => {
|
||||
return data;
|
||||
}).catch(error => {
|
||||
return error;
|
||||
});
|
||||
};
|
||||
|
||||
export const swipeBackOn = () => {
|
||||
return VKConnect.send("VKWebAppEnableSwipeBack", {}).then(data => {
|
||||
return data;
|
||||
}).catch(error => {
|
||||
return error;
|
||||
});
|
||||
};
|
||||
|
||||
export const swipeBackOff = () => {
|
||||
return VKConnect.send("VKWebAppDisableSwipeBack", {}).then(data => {
|
||||
return data;
|
||||
}).catch(error => {
|
||||
return error;
|
||||
});
|
||||
};
|
||||
|
||||
export const groupsGet = () => {
|
||||
return APICall('groups.get', {
|
||||
"extended": "1",
|
||||
"fields": "description",
|
||||
"count": "100"
|
||||
});
|
||||
};
|
||||
|
||||
export const APICall = (method, params) => {
|
||||
params['access_token'] = store.getState().vkui.accessToken;
|
||||
params['v'] = params['v'] === undefined ? API_VERSION : params['v'];
|
||||
|
||||
return VKConnect.send("VKWebAppCallAPIMethod", {
|
||||
"method": method,
|
||||
"params": params
|
||||
}).then(data => {
|
||||
return data.response;
|
||||
}).catch(error => {
|
||||
return error;
|
||||
});
|
||||
};
|
||||
42
src/js/services/_functions.js
Normal file
42
src/js/services/_functions.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import {store} from "../../index";
|
||||
|
||||
export const smoothScrollToTop = () => {
|
||||
const c = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
|
||||
if (c > 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (c > 0) {
|
||||
window.requestAnimationFrame(smoothScrollToTop);
|
||||
window.scrollTo(0, c - c / 8);
|
||||
}
|
||||
};
|
||||
|
||||
export const restoreScrollPosition = () => {
|
||||
let scrolls = store.getState().vkui.componentScroll;
|
||||
|
||||
Object.keys(scrolls).forEach((component) => {
|
||||
let componentData = scrolls[component];
|
||||
|
||||
let element = document.getElementById(component);
|
||||
|
||||
if (element) {
|
||||
element = element.getElementsByClassName("HorizontalScroll__in")[0];
|
||||
|
||||
element.scrollLeft = componentData.x;
|
||||
element.scrollTop = componentData.y;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getActivePanel = (view) => {
|
||||
let panel = store.getState().router.activePanel;
|
||||
|
||||
let panelsHistory = store.getState().router.panelsHistory;
|
||||
if (typeof panelsHistory[view] !== "undefined") {
|
||||
panel = panelsHistory[view][panelsHistory[view].length - 1];
|
||||
}
|
||||
|
||||
return panel;
|
||||
};
|
||||
16
src/js/services/renderers.js
Normal file
16
src/js/services/renderers.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import GroupCell from '../components/GroupCell';
|
||||
|
||||
export const renderGroupsList = (items) => {
|
||||
let groups = null;
|
||||
if (items !== undefined && items !== null && items.length !== 0) {
|
||||
groups = items.map((group) => (
|
||||
<GroupCell
|
||||
key={group.id}
|
||||
description={group.description}
|
||||
photo={group.photo_100}
|
||||
name={group.name}/>
|
||||
));
|
||||
}
|
||||
return groups;
|
||||
};
|
||||
1
src/js/store/formData/actionTypes.js
Normal file
1
src/js/store/formData/actionTypes.js
Normal file
@@ -0,0 +1 @@
|
||||
export const SET_FORM_DATA = 'SAVE_FORM_DATA';
|
||||
11
src/js/store/formData/actions.js
Normal file
11
src/js/store/formData/actions.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import {SET_FORM_DATA} from './actionTypes';
|
||||
|
||||
export const setFormData = (formName, inputData) => (
|
||||
{
|
||||
type: SET_FORM_DATA,
|
||||
payload: {
|
||||
form: formName,
|
||||
data: inputData,
|
||||
}
|
||||
}
|
||||
);
|
||||
27
src/js/store/formData/reducers.js
Normal file
27
src/js/store/formData/reducers.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import {SET_FORM_DATA} from './actionTypes';
|
||||
|
||||
const initialState = {
|
||||
forms: []
|
||||
};
|
||||
|
||||
export const formDataReducer = (state = initialState, action) => {
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case SET_FORM_DATA: {
|
||||
return {
|
||||
...state,
|
||||
forms: {
|
||||
...state.forms,
|
||||
[action.payload.form]: action.payload.data
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
10
src/js/store/reducers.js
Normal file
10
src/js/store/reducers.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {combineReducers} from "redux";
|
||||
import {routerReducer} from './router/reducers';
|
||||
import {vkuiReducer} from './vk/reducers';
|
||||
import {formDataReducer} from "./formData/reducers";
|
||||
|
||||
export default combineReducers({
|
||||
vkui: vkuiReducer,
|
||||
router: routerReducer,
|
||||
formData: formDataReducer
|
||||
});
|
||||
7
src/js/store/router/actionTypes.js
Normal file
7
src/js/store/router/actionTypes.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const SET_PAGE = 'SET_PAGE';
|
||||
export const SET_STORY = 'SET_STORY';
|
||||
export const GO_BACK = 'GO_BACK';
|
||||
export const OPEN_POPOUT = 'OPEN_POPOUT';
|
||||
export const CLOSE_POPOUT = 'CLOSE_POPOUT';
|
||||
export const OPEN_MODAL = 'OPEN_MODAL';
|
||||
export const CLOSE_MODAL = 'CLOSE_MODAL';
|
||||
57
src/js/store/router/actions.js
Normal file
57
src/js/store/router/actions.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import {SET_PAGE, SET_STORY, GO_BACK, OPEN_POPOUT, CLOSE_POPOUT, OPEN_MODAL, CLOSE_MODAL} from './actionTypes';
|
||||
|
||||
export const setStory = (story, initial_panel) => (
|
||||
{
|
||||
type: SET_STORY,
|
||||
payload: {
|
||||
story: story,
|
||||
initial_panel: initial_panel,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const setPage = (view, panel) => (
|
||||
{
|
||||
type: SET_PAGE,
|
||||
payload: {
|
||||
view: view,
|
||||
panel: panel
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const goBack = () => (
|
||||
{
|
||||
type: GO_BACK
|
||||
}
|
||||
);
|
||||
|
||||
export const openPopout = (popout) => (
|
||||
{
|
||||
type: OPEN_POPOUT,
|
||||
payload: {
|
||||
popout: popout
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const closePopout = () => (
|
||||
{
|
||||
type: CLOSE_POPOUT
|
||||
}
|
||||
);
|
||||
|
||||
export const openModal = (id) => (
|
||||
{
|
||||
type: OPEN_MODAL,
|
||||
payload: {
|
||||
id
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const closeModal = () => (
|
||||
{
|
||||
type: CLOSE_MODAL
|
||||
}
|
||||
);
|
||||
313
src/js/store/router/reducers.js
Normal file
313
src/js/store/router/reducers.js
Normal file
@@ -0,0 +1,313 @@
|
||||
import {
|
||||
SET_PAGE,
|
||||
GO_BACK,
|
||||
OPEN_POPOUT,
|
||||
CLOSE_POPOUT,
|
||||
OPEN_MODAL,
|
||||
CLOSE_MODAL,
|
||||
SET_STORY
|
||||
} from './actionTypes';
|
||||
|
||||
import * as VK from "../../services/VK";
|
||||
import {smoothScrollToTop} from "../../services/_functions";
|
||||
|
||||
const initialState = {
|
||||
activeStory: null,
|
||||
activeView: null,
|
||||
activePanel: null,
|
||||
|
||||
storiesHistory: [],
|
||||
viewsHistory: [],
|
||||
panelsHistory: [],
|
||||
|
||||
activeModals: [],
|
||||
modalHistory: [],
|
||||
popouts: [],
|
||||
|
||||
scrollPosition: []
|
||||
};
|
||||
|
||||
export const routerReducer = (state = initialState, action) => {
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case SET_PAGE: {
|
||||
let View = action.payload.view;
|
||||
let Panel = action.payload.panel;
|
||||
|
||||
window.history.pushState(null, null);
|
||||
|
||||
let panelsHistory = state.panelsHistory[View] || [];
|
||||
let viewsHistory = state.viewsHistory[state.activeStory] || [];
|
||||
|
||||
const viewIndexInHistory = viewsHistory.indexOf(View);
|
||||
|
||||
if (viewIndexInHistory !== -1) {
|
||||
viewsHistory.splice(viewIndexInHistory, 1);
|
||||
}
|
||||
|
||||
if (panelsHistory.indexOf(Panel) === -1) {
|
||||
panelsHistory = [...panelsHistory, Panel];
|
||||
}
|
||||
|
||||
if (panelsHistory.length > 1) {
|
||||
VK.swipeBackOn();
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeView: View,
|
||||
activePanel: Panel,
|
||||
|
||||
panelsHistory: {
|
||||
...state.panelsHistory,
|
||||
[View]: panelsHistory,
|
||||
},
|
||||
viewsHistory: {
|
||||
...state.viewsHistory,
|
||||
[state.activeStory]: [...viewsHistory, View]
|
||||
},
|
||||
scrollPosition: {
|
||||
...state.scrollPosition,
|
||||
[state.activeStory + "_" + state.activeView + "_" + state.activePanel]: window.pageYOffset
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case SET_STORY: {
|
||||
window.history.pushState(null, null);
|
||||
|
||||
let viewsHistory = state.viewsHistory[action.payload.story] || [action.payload.story];
|
||||
|
||||
let storiesHistory = state.storiesHistory;
|
||||
let activeView = viewsHistory[viewsHistory.length - 1];
|
||||
let panelsHistory = state.panelsHistory[activeView] || [action.payload.initial_panel];
|
||||
let activePanel = panelsHistory[panelsHistory.length - 1];
|
||||
|
||||
if (action.payload.story === state.activeStory) {
|
||||
if (panelsHistory.length > 1) {
|
||||
let firstPanel = panelsHistory.shift();
|
||||
panelsHistory = [firstPanel];
|
||||
|
||||
activePanel = panelsHistory[panelsHistory.length - 1];
|
||||
} else if (viewsHistory.length > 1) {
|
||||
let firstView = viewsHistory.shift();
|
||||
viewsHistory = [firstView];
|
||||
|
||||
activeView = viewsHistory[viewsHistory.length - 1];
|
||||
panelsHistory = state.panelsHistory[activeView];
|
||||
activePanel = panelsHistory[panelsHistory.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (action.payload.story === state.activeStory && panelsHistory.length === 1 && window.pageYOffset > 0) {
|
||||
window.scrollTo(0, 30);
|
||||
|
||||
smoothScrollToTop();
|
||||
}
|
||||
|
||||
const storiesIndexInHistory = storiesHistory.indexOf(action.payload.story);
|
||||
|
||||
if (storiesIndexInHistory === -1 || (storiesHistory[0] === action.payload.story && storiesHistory[storiesHistory.length - 1] !== action.payload.story)) {
|
||||
storiesHistory = [...storiesHistory, action.payload.story];
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeStory: action.payload.story,
|
||||
activeView: activeView,
|
||||
activePanel: activePanel,
|
||||
|
||||
storiesHistory: storiesHistory,
|
||||
viewsHistory: {
|
||||
...state.viewsHistory,
|
||||
[activeView]: viewsHistory
|
||||
},
|
||||
panelsHistory: {
|
||||
...state.panelsHistory,
|
||||
[activeView]: panelsHistory
|
||||
},
|
||||
|
||||
scrollPosition: {
|
||||
...state.scrollPosition,
|
||||
[state.activeStory + "_" + state.activeView + "_" + state.activePanel]: window.pageYOffset
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case GO_BACK: {
|
||||
let setView = state.activeView;
|
||||
let setPanel = state.activePanel;
|
||||
let setStory = state.activeStory;
|
||||
|
||||
let popoutsData = state.popouts;
|
||||
|
||||
if (popoutsData[setView]) {
|
||||
popoutsData[setView] = null;
|
||||
|
||||
return {
|
||||
...state,
|
||||
popouts: {
|
||||
...state.popouts, popoutsData
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let viewModalsHistory = state.modalHistory[setView];
|
||||
|
||||
if (viewModalsHistory !== undefined && viewModalsHistory.length !== 0) {
|
||||
let activeModal = viewModalsHistory[viewModalsHistory.length - 2] || null;
|
||||
|
||||
if (activeModal === null) {
|
||||
viewModalsHistory = [];
|
||||
} else if (viewModalsHistory.indexOf(activeModal) !== -1) {
|
||||
viewModalsHistory = viewModalsHistory.splice(0, viewModalsHistory.indexOf(activeModal) + 1);
|
||||
} else {
|
||||
viewModalsHistory.push(activeModal);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeModals: {
|
||||
...state.activeModals,
|
||||
[setView]: activeModal
|
||||
},
|
||||
modalHistory: {
|
||||
...state.modalHistory,
|
||||
[setView]: viewModalsHistory
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let panelsHistory = state.panelsHistory[setView] || [];
|
||||
let viewsHistory = state.viewsHistory[state.activeStory] || [];
|
||||
let storiesHistory = state.storiesHistory;
|
||||
|
||||
if (panelsHistory.length > 1) {
|
||||
panelsHistory.pop();
|
||||
|
||||
setPanel = panelsHistory[panelsHistory.length - 1];
|
||||
} else if (viewsHistory.length > 1) {
|
||||
viewsHistory.pop();
|
||||
|
||||
setView = viewsHistory[viewsHistory.length - 1];
|
||||
let panelsHistoryNew = state.panelsHistory[setView];
|
||||
|
||||
setPanel = panelsHistoryNew[panelsHistoryNew.length - 1];
|
||||
} else if (storiesHistory.length > 1) {
|
||||
storiesHistory.pop();
|
||||
|
||||
setStory = storiesHistory[storiesHistory.length - 1];
|
||||
setView = state.viewsHistory[setStory][state.viewsHistory[setStory].length - 1];
|
||||
|
||||
let panelsHistoryNew = state.panelsHistory[setView];
|
||||
|
||||
if (panelsHistoryNew.length > 1) {
|
||||
setPanel = panelsHistoryNew[panelsHistoryNew.length - 1];
|
||||
} else {
|
||||
setPanel = panelsHistoryNew[0];
|
||||
}
|
||||
} else {
|
||||
VK.closeApp();
|
||||
}
|
||||
|
||||
if (panelsHistory.length === 1) {
|
||||
VK.swipeBackOff();
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeView: setView,
|
||||
activePanel: setPanel,
|
||||
activeStory: setStory,
|
||||
|
||||
viewsHistory: {
|
||||
...state.viewsHistory,
|
||||
[state.activeView]: viewsHistory
|
||||
},
|
||||
panelsHistory: {
|
||||
...state.panelsHistory,
|
||||
[state.activeView]: panelsHistory
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case OPEN_POPOUT: {
|
||||
window.history.pushState(null, null);
|
||||
|
||||
return {
|
||||
...state,
|
||||
popouts: {
|
||||
...state.popouts,
|
||||
[state.activeView]: action.payload.popout
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case CLOSE_POPOUT: {
|
||||
return {
|
||||
...state,
|
||||
popouts: {
|
||||
...state.popouts,
|
||||
[state.activeView]: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case OPEN_MODAL: {
|
||||
window.history.pushState(null, null);
|
||||
|
||||
let activeModal = action.payload.id || null;
|
||||
let modalsHistory = state.modalHistory[state.activeView] ? [...state.modalHistory[state.activeView]] : [];
|
||||
|
||||
if (activeModal === null) {
|
||||
modalsHistory = [];
|
||||
} else if (modalsHistory.indexOf(activeModal) !== -1) {
|
||||
modalsHistory = modalsHistory.splice(0, modalsHistory.indexOf(activeModal) + 1);
|
||||
} else {
|
||||
modalsHistory.push(activeModal);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeModals: {
|
||||
...state.activeModals,
|
||||
[state.activeView]: activeModal
|
||||
},
|
||||
modalHistory: {
|
||||
...state.modalHistory,
|
||||
[state.activeView]: modalsHistory
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case CLOSE_MODAL: {
|
||||
let activeModal = state.modalHistory[state.activeView][state.modalHistory[state.activeView].length - 2] || null;
|
||||
let modalsHistory = state.modalHistory[state.activeView] ? [...state.modalHistory[state.activeView]] : [];
|
||||
|
||||
if (activeModal === null) {
|
||||
modalsHistory = [];
|
||||
} else if (modalsHistory.indexOf(activeModal) !== -1) {
|
||||
modalsHistory = modalsHistory.splice(0, modalsHistory.indexOf(activeModal) + 1);
|
||||
} else {
|
||||
modalsHistory.push(activeModal);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeModals: {
|
||||
...state.activeModals,
|
||||
[state.activeView]: activeModal
|
||||
},
|
||||
modalHistory: {
|
||||
...state.modalHistory,
|
||||
[state.activeView]: modalsHistory
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
5
src/js/store/vk/actionTypes.js
Normal file
5
src/js/store/vk/actionTypes.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const SET_COLOR_SCHEME = 'SET_COLOR_SCHEME';
|
||||
export const SET_ACCESS_TOKEN = 'SET_ACCESS_TOKEN';
|
||||
export const SET_ACTIVE_TAB = 'SET_ACTIVE_TAB';
|
||||
export const SET_SCROLL_POSITION = 'SET_SCROLL_POSITION';
|
||||
export const SET_SCROLL_POSITION_BY_ID = 'SET_SCROLL_POSITION_BY_ID';
|
||||
51
src/js/store/vk/actions.js
Normal file
51
src/js/store/vk/actions.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
SET_COLOR_SCHEME,
|
||||
SET_ACCESS_TOKEN,
|
||||
SET_ACTIVE_TAB,
|
||||
SET_SCROLL_POSITION,
|
||||
SET_SCROLL_POSITION_BY_ID
|
||||
} from './actionTypes';
|
||||
|
||||
export const setColorScheme = (scheme) => (
|
||||
{
|
||||
type: SET_COLOR_SCHEME,
|
||||
payload: scheme
|
||||
}
|
||||
);
|
||||
|
||||
export const setAccessToken = (accessToken) => (
|
||||
{
|
||||
type: SET_ACCESS_TOKEN,
|
||||
payload: accessToken
|
||||
}
|
||||
);
|
||||
|
||||
export const setActiveTab = (component, tab) => (
|
||||
{
|
||||
type: SET_ACTIVE_TAB,
|
||||
payload: {
|
||||
component,
|
||||
tab
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const setScrollPosition = (component, x = 0, y = 0) => (
|
||||
{
|
||||
type: SET_SCROLL_POSITION,
|
||||
payload: {
|
||||
component,
|
||||
x,
|
||||
y
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const setScrollPositionByID = (component) => (
|
||||
{
|
||||
type: SET_SCROLL_POSITION_BY_ID,
|
||||
payload: {
|
||||
component
|
||||
}
|
||||
}
|
||||
);
|
||||
80
src/js/store/vk/reducers.js
Normal file
80
src/js/store/vk/reducers.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
SET_ACCESS_TOKEN,
|
||||
SET_ACTIVE_TAB,
|
||||
SET_COLOR_SCHEME,
|
||||
SET_SCROLL_POSITION,
|
||||
SET_SCROLL_POSITION_BY_ID
|
||||
} from './actionTypes';
|
||||
|
||||
const initialState = {
|
||||
accessToken: undefined,
|
||||
colorScheme: 'client_light',
|
||||
|
||||
activeTab: [],
|
||||
componentScroll: []
|
||||
};
|
||||
|
||||
export const vkuiReducer = (state = initialState, action) => {
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case SET_COLOR_SCHEME: {
|
||||
return {
|
||||
...state,
|
||||
colorScheme: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case SET_ACCESS_TOKEN: {
|
||||
return {
|
||||
...state,
|
||||
accessToken: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case SET_ACTIVE_TAB: {
|
||||
return {
|
||||
...state,
|
||||
activeTab: {
|
||||
...state.activeTab,
|
||||
[action.payload.component]: action.payload.tab
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case SET_SCROLL_POSITION: {
|
||||
return {
|
||||
...state,
|
||||
componentScroll: {
|
||||
...state.componentScroll,
|
||||
[action.payload.component]: {
|
||||
x: action.payload.x,
|
||||
y: action.payload.y
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case SET_SCROLL_POSITION_BY_ID: {
|
||||
let element = document.getElementById(action.payload.component).getElementsByClassName("HorizontalScroll__in")[0];
|
||||
|
||||
let x = element.scrollLeft;
|
||||
let y = element.scrollTop;
|
||||
|
||||
return {
|
||||
...state,
|
||||
componentScroll: {
|
||||
...state.componentScroll,
|
||||
[action.payload.component]: {
|
||||
x: x,
|
||||
y: y
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user