This commit is contained in:
2023-02-11 00:33:00 +03:00
commit 6be8dce9f9
29 changed files with 1846 additions and 0 deletions

18
.gitignore vendored Normal file
View 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
View 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
View 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)

45
package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "vkminiappboilerplate",
"version": "1.0.0",
"description": "",
"main": "index.js",
"homepage": "https://andrewkydev.github.io/2",
"scripts": {
"predeploy": "export NODE_OPTIONS=--openssl-legacy-provider && npm run build",
"deploy": "export NODE_OPTIONS=--openssl-legacy-provider && rm -rf node_modules/.cache/gh-pages && gh-pages -d build",
"start": "export NODE_OPTIONS=--openssl-legacy-provider && cross-env PORT=10888 HTTPS=true 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",
"gh-pages": "^5.0.0",
"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
View 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
View 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
View 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
View 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
View 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')
);

View 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;

View 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));

View 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));

View 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);

View 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
View 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);

View 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
View 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;
});
};

View 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;
};

View 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;
};

View File

@@ -0,0 +1 @@
export const SET_FORM_DATA = 'SAVE_FORM_DATA';

View 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,
}
}
);

View 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
View 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
});

View 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';

View 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
}
);

View 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;
}
}
};

View 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';

View 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
}
}
);

View 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;
}
}
};