From e05528b890ee4dec37ccd88010bd4926e3e0636b Mon Sep 17 00:00:00 2001 From: andrewkydev Date: Fri, 10 Feb 2023 23:11:02 +0300 Subject: [PATCH] initial commit --- .gitignore | 18 + LICENSE | 21 ++ README.md | 93 ++++++ package.json | 39 +++ public/index.html | 13 + src/App.js | 154 +++++++++ src/AppWithoutEpic.js | 126 +++++++ src/css/main.css | 23 ++ src/index.js | 31 ++ src/js/components/GroupCell.js | 24 ++ src/js/components/modals/HomeBotInfoModal.js | 59 ++++ src/js/components/modals/HomeBotsListModal.js | 74 +++++ src/js/panels/home/base.js | 87 +++++ src/js/panels/home/groups.js | 145 ++++++++ src/js/panels/more/base.js | 109 ++++++ src/js/panels/more/example.js | 120 +++++++ src/js/services/VK.js | 84 +++++ src/js/services/_functions.js | 42 +++ src/js/services/renderers.js | 16 + src/js/store/formData/actionTypes.js | 1 + src/js/store/formData/actions.js | 11 + src/js/store/formData/reducers.js | 27 ++ src/js/store/reducers.js | 10 + src/js/store/router/actionTypes.js | 7 + src/js/store/router/actions.js | 57 ++++ src/js/store/router/reducers.js | 313 ++++++++++++++++++ src/js/store/vk/actionTypes.js | 5 + src/js/store/vk/actions.js | 51 +++ src/js/store/vk/reducers.js | 80 +++++ 29 files changed, 1840 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 package.json create mode 100644 public/index.html create mode 100644 src/App.js create mode 100644 src/AppWithoutEpic.js create mode 100644 src/css/main.css create mode 100644 src/index.js create mode 100644 src/js/components/GroupCell.js create mode 100644 src/js/components/modals/HomeBotInfoModal.js create mode 100644 src/js/components/modals/HomeBotsListModal.js create mode 100644 src/js/panels/home/base.js create mode 100644 src/js/panels/home/groups.js create mode 100644 src/js/panels/more/base.js create mode 100644 src/js/panels/more/example.js create mode 100644 src/js/services/VK.js create mode 100644 src/js/services/_functions.js create mode 100644 src/js/services/renderers.js create mode 100644 src/js/store/formData/actionTypes.js create mode 100644 src/js/store/formData/actions.js create mode 100644 src/js/store/formData/reducers.js create mode 100644 src/js/store/reducers.js create mode 100644 src/js/store/router/actionTypes.js create mode 100644 src/js/store/router/actions.js create mode 100644 src/js/store/router/reducers.js create mode 100644 src/js/store/vk/actionTypes.js create mode 100644 src/js/store/vk/actions.js create mode 100644 src/js/store/vk/reducers.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fd4ced --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fb806cc --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4bf82cb --- /dev/null +++ b/README.md @@ -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 ` + +Перейдите в созданную папку и выполните команды: `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 + +... + +``` +- Сохранить позицию при демонтировании +```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) diff --git a/package.json b/package.json new file mode 100644 index 0000000..b75cb76 --- /dev/null +++ b/package.json @@ -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" + ] +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..2403a07 --- /dev/null +++ b/public/index.html @@ -0,0 +1,13 @@ + + + + + + + + VK Mini App Boilerplate by iSa1vatore + + +
+ + \ No newline at end of file diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..b3e1c67 --- /dev/null +++ b/src/App.js @@ -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 = ( + + closeModal()} + /> + closeModal()} + /> + + ); + + return ( + + + setStory('home', 'base')} + selected={activeStory === 'home'} + > + setStory('more', 'callmodal')} + selected={activeStory === 'more'} + > + + }> + + goBack()} + > + + + + + + goBack()} + > + + + goBack()} + > + + + + + + ); + } +} + +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); diff --git a/src/AppWithoutEpic.js b/src/AppWithoutEpic.js new file mode 100644 index 0000000..df2c039 --- /dev/null +++ b/src/AppWithoutEpic.js @@ -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 = ( + + closeModal()} + /> + closeModal()} + /> + + ); + + return ( + + + goBack()} + > + + + + goBack()} + > + + + + + ); + } +} + +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); diff --git a/src/css/main.css b/src/css/main.css new file mode 100644 index 0000000..64ab35e --- /dev/null +++ b/src/css/main.css @@ -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; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..5b51bea --- /dev/null +++ b/src/index.js @@ -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( + + + , + document.getElementById('root') +); \ No newline at end of file diff --git a/src/js/components/GroupCell.js b/src/js/components/GroupCell.js new file mode 100644 index 0000000..6480b32 --- /dev/null +++ b/src/js/components/GroupCell.js @@ -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 ( + } + > + {name} + + ) + } + +} + +export default GroupCell; \ No newline at end of file diff --git a/src/js/components/modals/HomeBotInfoModal.js b/src/js/components/modals/HomeBotInfoModal.js new file mode 100644 index 0000000..e05c7da --- /dev/null +++ b/src/js/components/modals/HomeBotInfoModal.js @@ -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 ( + } + right={platform === IOS && + } + > + Информация о боте + + } + onClose={onClose} + settlingHeight={80} + > + } + > + Название + + + + + 8800 + + + + + 555 + + + + + 3535 + + + + + ); + } + +} + +export default withPlatform(connect()(HomeBotsListModal)); diff --git a/src/js/components/modals/HomeBotsListModal.js b/src/js/components/modals/HomeBotsListModal.js new file mode 100644 index 0000000..9be9c28 --- /dev/null +++ b/src/js/components/modals/HomeBotsListModal.js @@ -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 ( + } + right={platform === IOS && + } + > + /appbots на минималках + + } + onClose={onClose} + settlingHeight={80} + > + + {bots.map((bot, index) => ( + } + onClick={() => openModal('MODAL_PAGE_BOT_INFO')} + asideContent={} + > + {bot.name} + + ))} + + + ); + } + +} + +const mapDispatchToProps = { + openModal +}; + +export default withPlatform(connect(null, mapDispatchToProps)(HomeBotsListModal)); diff --git a/src/js/panels/home/base.js b/src/js/panels/home/base.js new file mode 100644 index 0000000..e241bd8 --- /dev/null +++ b/src/js/panels/home/base.js @@ -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( + this.props.closePopout()} + > +

Вопрос значит

+

Вас роняли в детстве?

+
+ ); + } + + 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 ( + + Examples + + {/*
+ +
*/} +
+

+ Ваш ник: {new URL(window.location.href).searchParams.get('payload') ? new URL(window.location.href).searchParams.get('payload') : ''} +

+
+ {/*
+

{params.get('payload')}

+
*/} +
+ +
+
+ +
+ +
+
+ ); + } + +} + +const mapDispatchToProps = { + setPage, + goBack, + openPopout, + closePopout, + openModal +}; + +export default connect(null, mapDispatchToProps)(HomePanelBase); diff --git a/src/js/panels/home/groups.js b/src/js/panels/home/groups.js new file mode 100644 index 0000000..9e238c4 --- /dev/null +++ b/src/js/panels/home/groups.js @@ -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 ( + + goBack()}/>} + > + Группы + + {this.state.loading && } + {!this.state.loading && this.state.errorGetAuthToken && +
Возникла ошибка при получении данных, возможно Вы не выдали права на список групп.
+
+ +
+
} + {!this.state.loading && !this.state.errorGetAuthToken && adminedGroupsList && + Администрируемые + }> + + {adminedGroupsList} + + } + {!this.state.loading && !this.state.errorGetAuthToken && otherGroupsList && + Остальные + }> + + {otherGroupsList} + + } +
+ ); + } + +} + +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); diff --git a/src/js/panels/more/base.js b/src/js/panels/more/base.js new file mode 100644 index 0000000..6ef3a1e --- /dev/null +++ b/src/js/panels/more/base.js @@ -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 ( + + Examples 2 + + + + this.setTab('modal')} + selected={this.state.activeTab === 'modal'} + > + Модальное окно + + this.setTab('test')} + selected={this.state.activeTab === 'test'} + > + Для теста + + this.setTab('test2')} + selected={this.state.activeTab === 'test2'} + > + Для теста 2 + + this.setTab('test3')} + selected={this.state.activeTab === 'test3'} + > + Для теста 3 + + + + + + {this.state.activeTab === 'modal' && setPage('modal', 'filters')}> + Открыть модальное окно + } + + {this.state.activeTab !== 'modal' &&
{this.state.activeTab}
} +
+
+ ); + } + +} + +const mapStateToProps = (state) => { + return { + activeTab: state.vkui.activeTab, + }; +}; + +const mapDispatchToProps = { + setPage, + setActiveTab, + setScrollPositionByID +}; + +export default connect(mapStateToProps, mapDispatchToProps)(HomePanelProfile); diff --git a/src/js/panels/more/example.js b/src/js/panels/more/example.js new file mode 100644 index 0000000..821bd55 --- /dev/null +++ b/src/js/panels/more/example.js @@ -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 ( + + goBack()}/>} + > + Модальное окно + + + + + + + + + С фотографией + Сейчас на сайте + + +
+ + +
+
+
+ ); + } + +} + +const mapStateToProps = (state) => { + return { + inputData: state.formData.forms, + }; +}; + +const mapDispatchToProps = { + setFormData, + goBack +}; + +export default connect(mapStateToProps, mapDispatchToProps)(HomePanelProfile); \ No newline at end of file diff --git a/src/js/services/VK.js b/src/js/services/VK.js new file mode 100644 index 0000000..cb9b3dc --- /dev/null +++ b/src/js/services/VK.js @@ -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; + }); +}; diff --git a/src/js/services/_functions.js b/src/js/services/_functions.js new file mode 100644 index 0000000..d2b4284 --- /dev/null +++ b/src/js/services/_functions.js @@ -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; +}; diff --git a/src/js/services/renderers.js b/src/js/services/renderers.js new file mode 100644 index 0000000..0b80b98 --- /dev/null +++ b/src/js/services/renderers.js @@ -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) => ( + + )); + } + return groups; +}; \ No newline at end of file diff --git a/src/js/store/formData/actionTypes.js b/src/js/store/formData/actionTypes.js new file mode 100644 index 0000000..d165d8a --- /dev/null +++ b/src/js/store/formData/actionTypes.js @@ -0,0 +1 @@ +export const SET_FORM_DATA = 'SAVE_FORM_DATA'; \ No newline at end of file diff --git a/src/js/store/formData/actions.js b/src/js/store/formData/actions.js new file mode 100644 index 0000000..f2b71c6 --- /dev/null +++ b/src/js/store/formData/actions.js @@ -0,0 +1,11 @@ +import {SET_FORM_DATA} from './actionTypes'; + +export const setFormData = (formName, inputData) => ( + { + type: SET_FORM_DATA, + payload: { + form: formName, + data: inputData, + } + } +); \ No newline at end of file diff --git a/src/js/store/formData/reducers.js b/src/js/store/formData/reducers.js new file mode 100644 index 0000000..6bed088 --- /dev/null +++ b/src/js/store/formData/reducers.js @@ -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; + } + + } + +}; \ No newline at end of file diff --git a/src/js/store/reducers.js b/src/js/store/reducers.js new file mode 100644 index 0000000..8c2defc --- /dev/null +++ b/src/js/store/reducers.js @@ -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 +}); \ No newline at end of file diff --git a/src/js/store/router/actionTypes.js b/src/js/store/router/actionTypes.js new file mode 100644 index 0000000..1d049d7 --- /dev/null +++ b/src/js/store/router/actionTypes.js @@ -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'; \ No newline at end of file diff --git a/src/js/store/router/actions.js b/src/js/store/router/actions.js new file mode 100644 index 0000000..fe11acf --- /dev/null +++ b/src/js/store/router/actions.js @@ -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 + } +); diff --git a/src/js/store/router/reducers.js b/src/js/store/router/reducers.js new file mode 100644 index 0000000..75fc085 --- /dev/null +++ b/src/js/store/router/reducers.js @@ -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; + } + } +}; diff --git a/src/js/store/vk/actionTypes.js b/src/js/store/vk/actionTypes.js new file mode 100644 index 0000000..e08f27b --- /dev/null +++ b/src/js/store/vk/actionTypes.js @@ -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'; \ No newline at end of file diff --git a/src/js/store/vk/actions.js b/src/js/store/vk/actions.js new file mode 100644 index 0000000..678daea --- /dev/null +++ b/src/js/store/vk/actions.js @@ -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 + } + } +); \ No newline at end of file diff --git a/src/js/store/vk/reducers.js b/src/js/store/vk/reducers.js new file mode 100644 index 0000000..27ed224 --- /dev/null +++ b/src/js/store/vk/reducers.js @@ -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; + } + } +}; \ No newline at end of file