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

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