├── .gitignore ├── view ├── frontend │ ├── web │ │ └── css │ │ │ ├── modules │ │ │ └── storelocator │ │ │ │ ├── _storelocator-variables.scss │ │ │ │ ├── _hours-wrapper.scss │ │ │ │ ├── _storelocator.scss │ │ │ │ ├── _store-view.scss │ │ │ │ ├── _tabs.scss │ │ │ │ ├── _stores-li.scss │ │ │ │ ├── _directionstab.scss │ │ │ │ └── _storelocator-header.scss │ │ │ └── styles.scss │ ├── preact │ │ ├── src │ │ │ ├── util │ │ │ │ ├── Camelize.js │ │ │ │ ├── GoogleApi.js │ │ │ │ └── ScriptCache.js │ │ │ ├── component │ │ │ │ ├── RegionFilter.js │ │ │ │ ├── HoursSpanFill.js │ │ │ │ ├── HoursFormatter.js │ │ │ │ ├── HoursSelectFill.js │ │ │ │ ├── SingleStore.js │ │ │ │ ├── Marker.js │ │ │ │ ├── GoogleApiComponent.js │ │ │ │ └── Map.js │ │ │ ├── index.js │ │ │ ├── container │ │ │ │ ├── StoresList.js │ │ │ │ ├── Directions.js │ │ │ │ ├── HeaderMap.js │ │ │ │ ├── DirectionsTab.js │ │ │ │ ├── StoreView.js │ │ │ │ └── StoreHeader.js │ │ │ ├── App.js │ │ │ └── store │ │ │ │ └── index.js │ │ ├── .babelrc │ │ ├── webpack │ │ │ ├── production.config.js │ │ │ ├── dev.config.js │ │ │ └── base.config.js │ │ ├── .eslintrc │ │ └── package.json │ ├── requirejs-config.js │ └── layout │ │ └── storelocator_index_index.xml └── adminhtml │ ├── web │ └── js │ │ ├── fetchCoordsButton.js │ │ ├── fetchCoords.js │ │ └── fetchRegionsForCountry.js │ ├── layout │ ├── storelocator_regions_index.xml │ ├── storelocator_index_edit.xml │ ├── storelocator_regions_edit.xml │ └── storelocator_index_index.xml │ └── templates │ └── system │ └── config │ └── coordinates_button.phtml ├── registration.php ├── composer.json ├── Block ├── Storelocator.php └── Adminhtml │ └── System │ └── Config │ └── ButtonCoordinates.php ├── etc ├── module.xml ├── frontend │ ├── routes.xml │ └── di.xml ├── adminhtml │ ├── routes.xml │ ├── menu.xml │ └── system.xml ├── config.xml ├── acl.xml └── di.xml ├── Model ├── CountriesData.php ├── ResourceModel │ ├── RegionsData.php │ ├── CountriesData.php │ ├── States.php │ ├── StoreLocator.php │ ├── States │ │ └── Collection.php │ ├── CountriesData │ │ └── Collection.php │ ├── RegionsData │ │ └── Collection.php │ └── StoreLocator │ │ ├── Collection.php │ │ └── Grid │ │ └── Collection.php ├── RegionsData.php ├── Config │ ├── Backend │ │ └── Image.php │ └── Source │ │ ├── ListTimeFormat.php │ │ ├── ListStoreStatus.php │ │ ├── ListCountry.php │ │ ├── ListZoom.php │ │ ├── ListOpenHours.php │ │ ├── ListCloseHours.php │ │ └── ListState.php ├── States │ └── DataProvider.php ├── StoreLocator │ └── DataProvider.php ├── StatesRepository.php ├── StoreLocatorRepository.php └── GoogleApi.php ├── Logger ├── Handler.php └── Logger.php ├── Controller ├── Index │ ├── View.php │ ├── Index.php │ └── Json.php ├── Adminhtml │ ├── Regions │ │ ├── DeleteUnused.php │ │ ├── MassDelete.php │ │ ├── Delete.php │ │ ├── Update.php │ │ ├── Index.php │ │ ├── Edit.php │ │ ├── InlineEdit.php │ │ └── Save.php │ └── Index │ │ ├── NewAction.php │ │ ├── MassDelete.php │ │ ├── Index.php │ │ ├── Delete.php │ │ ├── Edit.php │ │ └── InlineEdit.php ├── Regions │ ├── Update.php │ └── GetByCountry.php └── Router.php ├── README.md ├── LICENCE ├── Helper ├── FrontProvider.php └── ConfigProvider.php ├── Api ├── Data │ ├── StatesInterface.php │ └── StoreLocatorInterface.php ├── StatesRepositoryInterface.php └── StoreLocatorRepositoryInterface.php ├── Setup └── Uninstall.php └── Ui └── Component └── Listing └── Column ├── RegionsActions.php └── IndexActions.php /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.map 3 | -------------------------------------------------------------------------------- /view/frontend/web/css/modules/storelocator/_storelocator-variables.scss: -------------------------------------------------------------------------------- 1 | $black: #242424; 2 | $grey: #9b9999; -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | str 3 | .split(' ') 4 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 5 | .join(''); 6 | 7 | export default camelize; 8 | -------------------------------------------------------------------------------- /view/frontend/requirejs-config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | deps: [ 3 | "PandaGroup_StoreLocator/dist/js" 4 | ], 5 | bundles: { 6 | "PandaGroup_StoreLocator/dist/js": ["store-locator"] 7 | } 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /Block/Storelocator.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /view/frontend/preact/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "plugins": [ 11 | "jsx-a11y" 12 | ], 13 | "rules": { 14 | "semi": 2 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /view/frontend/preact/src/component/RegionFilter.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const RegionFilter = props => { 4 | const addFilter = () => { 5 | props.onFilterClick(props.region); 6 | }; 7 | return ( 8 |

9 | {props.region} 10 |

11 | ); 12 | }; 13 | 14 | export default RegionFilter; 15 | -------------------------------------------------------------------------------- /view/frontend/preact/webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var config = require('./base.config'); 4 | 5 | config.output.path = path.join( 6 | __dirname, 7 | './../../../../../../../../pub/static/frontend/peterjacksons/petertheme/en_AU/PandaGroup_StoreLocator/dist' 8 | ); 9 | 10 | module.exports = config; 11 | -------------------------------------------------------------------------------- /Model/CountriesData.php: -------------------------------------------------------------------------------- 1 | _init('PandaGroup\StoreLocator\Model\ResourceModel\CountriesData'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Model/ResourceModel/RegionsData.php: -------------------------------------------------------------------------------- 1 | _init('storelocator_data_regions', 'id'); 13 | } 14 | } -------------------------------------------------------------------------------- /etc/frontend/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Model/ResourceModel/CountriesData.php: -------------------------------------------------------------------------------- 1 | _init('storelocator_data_countries', 'id'); 13 | } 14 | } -------------------------------------------------------------------------------- /Model/ResourceModel/States.php: -------------------------------------------------------------------------------- 1 | _init('storelocator_states', 'state_id'); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Model/ResourceModel/StoreLocator.php: -------------------------------------------------------------------------------- 1 | _init('storelocator', 'storelocator_id'); 13 | } 14 | } -------------------------------------------------------------------------------- /etc/adminhtml/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/fetchCoordsButton.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'jquery', 3 | 'Magento_Ui/js/form/components/button', 4 | 'PandaGroup_StoreLocator/js/fetchCoords' 5 | ], function ( 6 | $, 7 | Button, 8 | fetchCoords 9 | ) { 10 | 'use strict'; 11 | 12 | return Button.extend({ 13 | action: function () { 14 | fetchCoords(); 15 | } 16 | }); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /view/frontend/web/css/modules/storelocator/_hours-wrapper.scss: -------------------------------------------------------------------------------- 1 | .store-view { 2 | .hours-wrapper { 3 | &__day { 4 | display: block; 5 | } 6 | 7 | &__day-name { 8 | font-weight: bold; 9 | display: inline-block; 10 | width: 35px; 11 | margin: 0 5px 8px 0; 12 | text-align: right; 13 | } 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /view/frontend/preact/src/component/HoursSpanFill.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import HoursFormatter from './HoursFormatter'; 3 | 4 | const HoursSpanFill = props => { 5 | return ( 6 |
7 | {Object.keys(props.day).map(day => ( 8 | 9 | ))} 10 |
11 | ); 12 | }; 13 | 14 | export default HoursSpanFill; 15 | -------------------------------------------------------------------------------- /view/adminhtml/layout/storelocator_regions_index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Logger/Handler.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /view/adminhtml/layout/storelocator_regions_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Model/ResourceModel/States/Collection.php: -------------------------------------------------------------------------------- 1 | _init( 13 | 'PandaGroup\StoreLocator\Model\States', 14 | 'PandaGroup\StoreLocator\Model\ResourceModel\States' 15 | ); 16 | } 17 | } -------------------------------------------------------------------------------- /Model/ResourceModel/CountriesData/Collection.php: -------------------------------------------------------------------------------- 1 | _init( 13 | 'PandaGroup\StoreLocator\Model\CountriesData', 14 | 'PandaGroup\StoreLocator\Model\ResourceModel\CountriesData' 15 | ); 16 | } 17 | } -------------------------------------------------------------------------------- /view/frontend/web/css/styles.scss: -------------------------------------------------------------------------------- 1 | @import './../../web/css/helpers/variables'; 2 | @import './../../web/css/helpers/sprite'; 3 | @import './../../web/css/helpers/mixins'; 4 | 5 | @import "modules/storelocator/storelocator-variables"; 6 | @import "modules/storelocator/storelocator"; 7 | @import "modules/storelocator/stores-li"; 8 | @import "modules/storelocator/storelocator-header"; 9 | @import "modules/storelocator/store-view"; 10 | @import "modules/storelocator/tabs"; 11 | @import "modules/storelocator/hours-wrapper"; 12 | @import "modules/storelocator/directionstab"; -------------------------------------------------------------------------------- /view/frontend/preact/src/component/HoursFormatter.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const HoursFormatter = props => { 4 | const closedCheck = hours_arr => { 5 | return hours_arr[0] == '12:00 AM' && hours_arr[1] == '12:00 AM' 6 | ? 'Closed' 7 | : hours_arr.join(' - '); 8 | }; 9 | 10 | const Wrapper = props.wrapper; 11 | 12 | return ( 13 | 14 | {props.day}: {closedCheck(props.hours)} 15 | 16 | ); 17 | }; 18 | 19 | export default HoursFormatter; 20 | -------------------------------------------------------------------------------- /view/frontend/preact/src/component/HoursSelectFill.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import HoursFormatter from './HoursFormatter'; 3 | 4 | const HoursSelectFill = props => { 5 | return ( 6 |
7 | 13 |
14 |
15 | ); 16 | }; 17 | 18 | export default HoursSelectFill; 19 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 24 8 | 9 | 10 | 1 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /view/frontend/layout/storelocator_index_index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |         8 |             9 |         10 | 11 |     12 | -------------------------------------------------------------------------------- /view/frontend/web/css/modules/storelocator/_storelocator.scss: -------------------------------------------------------------------------------- 1 | .storelocator-index-index { 2 | 3 | .page-main { 4 | max-width: 100%; 5 | min-height: 50vh; 6 | padding: 0; 7 | position: relative; 8 | 9 | } 10 | 11 | #preact-loader { 12 | text-align: center; 13 | font-size: 16px; 14 | position: absolute; 15 | top: 50%; 16 | left: 50% 17 | } 18 | } 19 | 20 | .storelocator { 21 | 22 | margin-bottom: 50px; 23 | 24 | a, .storelocator-header__filter, .stores-li__name { 25 | color: #222; 26 | 27 | &:hover { 28 | color: #999; 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /etc/frontend/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | PandaGroup\StoreLocator\Controller\Router 9 | false 10 | 32 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Controller/Index/View.php: -------------------------------------------------------------------------------- 1 | redirectFactory = $context->getResultRedirectFactory(); 19 | parent::__construct($context); 20 | } 21 | 22 | public function execute() 23 | { 24 | return $this->redirectFactory->create()->setPath('storelocator'); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /view/frontend/preact/src/index.js: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | import App from './App'; 3 | import 'whatwg-fetch'; 4 | require('es6-promise').polyfill(); 5 | 6 | const dots = loader => { 7 | let i = 0; 8 | return setInterval(function() { 9 | i++; 10 | loader.insertAdjacentHTML('beforeend', '.'); 11 | if (i >= 4) { 12 | i = 0; 13 | loader.querySelectorAll('span').forEach(span => { 14 | span.remove(); 15 | }); 16 | } 17 | }, 500); 18 | }; 19 | 20 | export function init() { 21 | const loader = document.getElementById('preact-loader'); 22 | const intrv = dots(loader); 23 | fetch('/storelocator/index/json') 24 | .then(data => data.json()) 25 | .then(json => { 26 | clearInterval(intrv); 27 | loader.remove(); 28 | render(, document.getElementById('preact-root')); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /Model/RegionsData.php: -------------------------------------------------------------------------------- 1 | _init('PandaGroup\StoreLocator\Model\ResourceModel\RegionsData'); 13 | } 14 | 15 | public function findRegionByName($regionName, $countryName) 16 | { 17 | $regionsDataCollection = $this->getCollection(); 18 | 19 | $regionsDataCollection 20 | ->addFilter('secondTable.code', strtolower($countryName)) 21 | ->addFilter('main_table.name', $regionName); 22 | 23 | foreach ($regionsDataCollection as $regionData) { 24 | return $regionData->getData(); 25 | } 26 | 27 | return null; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Model/ResourceModel/RegionsData/Collection.php: -------------------------------------------------------------------------------- 1 | _init( 13 | 'PandaGroup\StoreLocator\Model\RegionsData', 14 | 'PandaGroup\StoreLocator\Model\ResourceModel\RegionsData' 15 | ); 16 | } 17 | 18 | protected function _initSelect() 19 | { 20 | parent::_initSelect(); 21 | 22 | $this->getSelect()->joinLeft( 23 | ['secondTable' => $this->getTable('storelocator_data_countries')], 24 | 'main_table.country_id = secondTable.id', 25 | ['country_name' => 'name', 'country_code' => 'code'] 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /view/frontend/preact/webpack/base.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: { 7 | js: './src/index.js', 8 | vendor: ['preact'], 9 | }, 10 | output: { 11 | path: '', 12 | filename: '[name].js', 13 | libraryTarget: 'amd', 14 | library: 'store-locator', 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.css$/, 20 | exclude: /node_modules/, 21 | use: ['style-loader', 'css-loader'], 22 | }, 23 | { 24 | test: /\.(js|jsx)$/, 25 | exclude: /node_modules/, 26 | use: ['babel-loader', 'eslint-loader'], 27 | }, 28 | ], 29 | }, 30 | resolve: { 31 | alias: { 32 | react: 'preact-compat', 33 | 'react-dom': 'preact-compat', 34 | }, 35 | extensions: ['.webpack-loader.js', '.web-loader.js', '.loader.js', '.js', '.jsx'], 36 | modules: [path.resolve(__dirname, '../node_modules')], 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /view/adminhtml/layout/storelocator_index_index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /view/frontend/preact/src/container/StoresList.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { connect } from 'mobx-preact'; 3 | 4 | import SingleStore from './../component/SingleStore'; 5 | 6 | @connect(['stateStore']) 7 | export default class StoresList extends Component { 8 | constructor() { 9 | super(); 10 | this.applyZoom = this.applyZoom.bind(this); 11 | this.changeView = this.changeView.bind(this); 12 | } 13 | 14 | componentDidMount() { 15 | document.title = 'Store Locator'; 16 | } 17 | 18 | applyZoom(gps, zoom) { 19 | this.props.stateStore.changeMap(gps, zoom); 20 | } 21 | 22 | changeView() { 23 | this.props.stateStore.changeView(); 24 | } 25 | 26 | render() { 27 | const { stores } = this.props.stateStore; 28 | return ( 29 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /view/frontend/web/css/modules/storelocator/_store-view.scss: -------------------------------------------------------------------------------- 1 | .store-view { 2 | max-width: 960px; 3 | margin: 20px auto; 4 | display: flex; 5 | flex-wrap: wrap; 6 | 7 | div { 8 | box-sizing: border-box; 9 | } 10 | 11 | &__store-card, &__tabs { 12 | flex: 1 1 50%; 13 | padding: 10px; 14 | } 15 | 16 | &__name { 17 | font-size: 40px; 18 | font-family: 'Roboto', sans-serif; 19 | font-weight: normal; 20 | margin: 0 0 20px 0; 21 | } 22 | 23 | &__credentials { 24 | li { 25 | display: flex; 26 | margin-bottom: 8px; 27 | } 28 | } 29 | 30 | &__label { 31 | flex: 0 0 75px; 32 | display: block; 33 | font-weight: bold; 34 | } 35 | 36 | @media screen and (max-width: $sm-breakpoint) { 37 | &__store-card, &__tabs { 38 | flex: 1 1 100%; 39 | } 40 | 41 | &__name { 42 | margin-bottom: 35px; 43 | } 44 | } 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Store Locator module for Magento 2.x 2 | 3 | # Installation 4 | ``` 5 | $ cd 6 | $ git clone https://github.com/pandagrouppl/StoreLocator app/code/PandaGroup/StoreLocator 7 | $ cd app/code/PandaGroup/StoreLocator/view/frontend/preact/ 8 | $ yarn install 9 | ``` 10 | # Usage 11 | 12 | ``` 13 | $ cd app/code/PandaGroup/StoreLocator/view/frontend/preact/ 14 | $ yarn local/server 15 | ``` 16 | 17 | depending on environment, use ` local ` or `server` 18 | - `dev` will push files to `/pub/static/../dist`, 19 | - `production` will push them to `app/../StoreLocator/../dist` directory, static content deploy required after running. 20 | 21 | # Authors 22 | 23 | Tomasz Jezierski [@Thorleon](https://github.com/Thorleon) tjezierski@pandagroup.co 24 | 25 | Adrian Kowalczewski [@adyry](https://github.com/adyry) [akowalczewski@pandagroup.co](mailto:akowalczewski@light4website.com) 26 | 27 | Karol Brzozowski [@brzozowski96](https://github.com/brzozowski96) [kbrzozowski@pandagroup.co](mailto:kbrzozowski@light4website.com) 28 | 29 | Property of Panda Group, [@pandagrouppl](https://github.com/pandagrouppl) -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Panda Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Controller/Index/Index.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 20 | parent::__construct($context); 21 | } 22 | 23 | /** 24 | * Blog Index, shows a list of recent blog posts. 25 | * 26 | * @return \Magento\Framework\View\Result\PageFactory 27 | */ 28 | public function execute() 29 | { 30 | $resultPage = $this->resultPageFactory->create(); 31 | $resultPage->getConfig()->getTitle()->set(__('Store Locator')); 32 | return $resultPage; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Model/Config/Backend/Image.php: -------------------------------------------------------------------------------- 1 | _mediaDirectory->getAbsolutePath($this->_appendScopeInfo(self::UPLOAD_DIR)); 22 | } 23 | 24 | /** 25 | * Makes a decision about whether to add info about the scope. 26 | * 27 | * @return boolean 28 | */ 29 | protected function _addWhetherScopeInfo() 30 | { 31 | return true; 32 | } 33 | 34 | /** 35 | * Getter for allowed extensions of uploaded files. 36 | * 37 | * @return string[] 38 | */ 39 | protected function _getAllowedExtensions() 40 | { 41 | return ['jpg', 'jpeg', 'gif', 'png', 'svg']; 42 | } 43 | } -------------------------------------------------------------------------------- /view/frontend/web/css/modules/storelocator/_tabs.scss: -------------------------------------------------------------------------------- 1 | .store-view { 2 | .tabs { 3 | &__tab-bar { 4 | 5 | display: flex; 6 | background-color: $grey; 7 | color: white; 8 | text-transform: uppercase; 9 | font-weight: bold; 10 | 11 | > div { 12 | flex: 0 0 50%; 13 | text-align: center; 14 | padding: 10px 0; 15 | } 16 | } 17 | 18 | &__tab { 19 | cursor: pointer; 20 | 21 | &--active { 22 | background-color: $black; 23 | } 24 | 25 | &:not(:first-child) { 26 | border-left: 1px solid #e4e0e0; 27 | } 28 | } 29 | 30 | &__tab-body { 31 | padding: 10px; 32 | border: 1px solid #e4e0e0; 33 | } 34 | 35 | &__h2 { 36 | font-family: league_gothic; 37 | font-size: 20px; 38 | font-weight: normal; 39 | text-transform: uppercase; 40 | color: $black; 41 | text-align: left; 42 | margin: 10px 0 15px 0; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Helper/FrontProvider.php: -------------------------------------------------------------------------------- 1 | storeManager = $storeManager; 27 | $this->states = $states; 28 | parent::__construct($context); 29 | } 30 | 31 | /** 32 | * Returns array collection of all states 33 | * 34 | * @return array 35 | */ 36 | public function getNavTabDataAsArray() 37 | { 38 | return $this->states->getStatesCollection()->getData(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Model/Config/Source/ListTimeFormat.php: -------------------------------------------------------------------------------- 1 | _catalogConfig = $catalogConfig; 22 | } 23 | 24 | /** 25 | * Retrieve option values array 26 | * 27 | * @return array 28 | */ 29 | public function toOptionArray() 30 | { 31 | $options = []; 32 | $options[] = ['label' => __('12-hour'), 'value' => 12]; 33 | $options[] = ['label' => __('24-hour'), 'value' => 24]; 34 | return $options; 35 | } 36 | 37 | /** 38 | * Retrieve Catalog Config Singleton 39 | * 40 | * @return \Magento\Catalog\Model\Config 41 | */ 42 | protected function _getCatalogConfig() 43 | { 44 | return $this->_catalogConfig; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Model/Config/Source/ListStoreStatus.php: -------------------------------------------------------------------------------- 1 | _catalogConfig = $catalogConfig; 22 | } 23 | 24 | /** 25 | * Retrieve option values array 26 | * 27 | * @return array 28 | */ 29 | public function toOptionArray() 30 | { 31 | $options = []; 32 | $options[] = ['label' => __('Enable'), 'value' => 1]; 33 | $options[] = ['label' => __('Disable'), 'value' => 0]; 34 | return $options; 35 | } 36 | 37 | /** 38 | * Retrieve Catalog Config Singleton 39 | * 40 | * @return \Magento\Catalog\Model\Config 41 | */ 42 | protected function _getCatalogConfig() 43 | { 44 | return $this->_catalogConfig; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /view/frontend/preact/src/component/SingleStore.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import HoursSelectFill from './HoursSelectFill'; 5 | 6 | const SingleStore = props => { 7 | const zoomToStore = () => { 8 | props.onStoreClick(props.geo, props.zoom); 9 | }; 10 | 11 | const zoomToStoreLink = () => { 12 | props.onStoreClick(props.geo, props.zoom); 13 | props.onLinkClick(); 14 | }; 15 | 16 | return ( 17 |
  • 18 |
    19 | 26 |

    {props.name}

    27 | 28 |
      29 |
    • 30 | {props.addr_strt} {props.addr_cty} {props.zipcode} 31 |
    • 32 |
    • 33 | {props.phone} 34 |
    • 35 |
    • 36 | {props.email} 37 |
    • 38 |
    39 |
    40 | 41 |
  • 42 | ); 43 | }; 44 | 45 | export default SingleStore; 46 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Regions/DeleteUnused.php: -------------------------------------------------------------------------------- 1 | resultRedirectFactory->create(); 16 | 17 | $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); 18 | /** @var \PandaGroup\StoreLocator\Model\States $statesModel */ 19 | $statesModel = $objectManager->create('\PandaGroup\StoreLocator\Model\States'); 20 | 21 | $qtyOfDeleted = $statesModel->deleteUnused(); 22 | 23 | if ($qtyOfDeleted > 0) { 24 | $this->messageManager->addSuccessMessage($qtyOfDeleted . __(' unused regions was deleted.')); 25 | } elseif ($qtyOfDeleted === 0) { 26 | $this->messageManager->addSuccessMessage(__('All of regions are used to some stores.')); 27 | } else { 28 | $this->messageManager->addErrorMessage(__('Something went wrong while deleting regions.')); 29 | } 30 | 31 | return $resultRedirect->setPath('*/*/'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Index/NewAction.php: -------------------------------------------------------------------------------- 1 | resultForwardFactory = $resultForwardFactory; 24 | parent::__construct($context); 25 | } 26 | 27 | /** 28 | * Create new store 29 | * 30 | * @return \Magento\Framework\Controller\ResultInterface 31 | */ 32 | public function execute() 33 | { 34 | /** @var \Magento\Framework\Controller\Result\Forward $resultForward */ 35 | $resultForward = $this->resultForwardFactory->create(); 36 | 37 | return $resultForward->forward('edit'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Index/MassDelete.php: -------------------------------------------------------------------------------- 1 | resultRedirectFactory->create(); 16 | 17 | $qtyOfDeleted = 0; 18 | $selectedIds = $this->getRequest()->getParam('selected'); 19 | foreach ($selectedIds as $id) { 20 | if ($id) { 21 | try { 22 | $id = (int) $id; 23 | $model = $this->_objectManager->create('PandaGroup\StoreLocator\Model\StoreLocator'); 24 | $model->load($id); 25 | $model->delete(); 26 | 27 | $qtyOfDeleted++; 28 | 29 | } catch (\Exception $e) { 30 | $this->messageManager->addErrorMessage($e->getMessage()); 31 | } 32 | } 33 | } 34 | 35 | $this->messageManager->addSuccessMessage(__('You deleted '. $qtyOfDeleted .' selected stores.')); 36 | 37 | return $resultRedirect->setPath('*/*/'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /view/frontend/preact/src/component/Marker.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | import camelize from '../util/Camelize'; 4 | 5 | const evtNames = ['click', 'mouseover', 'recenter', 'dragend']; 6 | 7 | export default class Marker extends Component { 8 | componentDidMount() { 9 | this.renderMarker(); 10 | } 11 | 12 | componentDidUpdate(prevProps) { 13 | if (this.props.map !== prevProps.map || this.props.position !== prevProps.position) { 14 | if (this.marker) { 15 | this.marker.setMap(null); 16 | } 17 | this.renderMarker(); 18 | } 19 | } 20 | 21 | componentWillUnmount() { 22 | if (this.marker) { 23 | this.marker.setMap(null); 24 | } 25 | } 26 | 27 | renderMarker() { 28 | let { map, google, position, mapCenter, icon } = this.props; 29 | 30 | if (!google) { 31 | return null; 32 | } 33 | 34 | let pos = position || mapCenter; 35 | position = new google.maps.LatLng(pos.lat, pos.lng); 36 | 37 | const pref = { 38 | map: map, 39 | position: position, 40 | icon: icon, 41 | }; 42 | this.marker = new google.maps.Marker(pref); 43 | 44 | this.marker.addListener( 45 | 'click', 46 | function() { 47 | const evtName = `onClick`; 48 | if (this.props[evtName]) { 49 | this.props[evtName](this.props); 50 | } 51 | }.bind(this) 52 | ); 53 | } 54 | 55 | render() { 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Regions/MassDelete.php: -------------------------------------------------------------------------------- 1 | resultRedirectFactory->create(); 16 | 17 | $qtyOfDeleted = 0; 18 | $selectedIds = $this->getRequest()->getParam('selected'); 19 | foreach ($selectedIds as $id) { 20 | if ($id) { 21 | try { 22 | $id = (int) $id; 23 | $model = $this->_objectManager->create('PandaGroup\StoreLocator\Model\States'); 24 | $model->load($id); 25 | $model->delete(); 26 | 27 | $qtyOfDeleted++; 28 | 29 | } catch (\Exception $e) { 30 | $this->messageManager->addErrorMessage($e->getMessage()); 31 | } 32 | } 33 | } 34 | 35 | $this->messageManager->addSuccessMessage(__('You deleted '. $qtyOfDeleted .' selected regions. Check your stores which used this region.')); 36 | 37 | return $resultRedirect->setPath('*/*/'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Controller/Regions/Update.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 26 | $this->storeLocatorModel = $storeLocatorModel; 27 | parent::__construct($context); 28 | } 29 | 30 | /** 31 | * Update all stores regions in the database, which have incorrect region 32 | */ 33 | public function execute() 34 | { 35 | $this->storeLocatorModel->updateRegions(); 36 | 37 | $this->_redirect('storelocator'); 38 | 39 | return; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Regions/Delete.php: -------------------------------------------------------------------------------- 1 | resultRedirectFactory->create(); 16 | 17 | $id = (int) $this->getRequest()->getParam('id'); 18 | if ($id) { 19 | try { 20 | 21 | $model = $this->_objectManager->create('PandaGroup\StoreLocator\Model\States'); 22 | $model->load($id); 23 | $model->delete(); 24 | 25 | $this->messageManager->addSuccessMessage(__('You deleted the region. Check your stores which used this region.')); 26 | 27 | return $resultRedirect->setPath('*/*/'); 28 | } catch (\Exception $e) { 29 | 30 | $this->messageManager->addErrorMessage($e->getMessage()); 31 | 32 | return $resultRedirect->setPath('*/*/edit', ['id' => $id]); 33 | } 34 | } 35 | 36 | $this->messageManager->addErrorMessage(__('We can\'t find a region to delete.')); 37 | 38 | return $resultRedirect->setPath('*/*/'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Regions/Update.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 27 | $this->storeLocatorModel = $storeLocatorModel; 28 | parent::__construct($context); 29 | } 30 | 31 | /** 32 | * Update all stores regions in the database, which have incorrect region 33 | */ 34 | public function execute() 35 | { 36 | $this->storeLocatorModel->updateRegions(); 37 | 38 | $this->_redirect('storelocator'); 39 | 40 | return; 41 | } 42 | } -------------------------------------------------------------------------------- /view/frontend/preact/src/util/GoogleApi.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | 3 | export const GoogleApi = function(opts) { 4 | opts = opts || {}; 5 | 6 | invariant(opts.hasOwnProperty('apiKey'), 7 | 'You must pass an apiKey to use GoogleApi'); 8 | 9 | const apiKey = opts.apiKey; 10 | const libraries = opts.libraries || ['places']; 11 | const client = opts.client; 12 | const URL = 'https://maps.googleapis.com/maps/api/js'; 13 | 14 | const googleVersion = opts.version || '3'; 15 | 16 | let script = null; 17 | let google = window.google || null; 18 | let loading = false; 19 | let channel = null; 20 | let language = null; 21 | let region = null; 22 | 23 | let onLoadEvents = []; 24 | 25 | const url = () => { 26 | let url = URL; 27 | let params = { 28 | key: apiKey, 29 | callback: 'CALLBACK_NAME', 30 | libraries: libraries.join(','), 31 | client: client, 32 | v: googleVersion, 33 | channel: channel, 34 | language: language, 35 | region: region 36 | }; 37 | 38 | let paramStr = Object.keys(params) 39 | .filter(k => !!params[k]) 40 | .map(k => `${k}=${params[k]}`).join('&'); 41 | 42 | return `${url}?${paramStr}`; 43 | }; 44 | 45 | return url(); 46 | }; 47 | 48 | export default GoogleApi; 49 | -------------------------------------------------------------------------------- /view/frontend/preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magento2-storelocator", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack --watch --config webpack/dev.config.js", 8 | "production": "webpack -p --config webpack/production.config.js" 9 | }, 10 | "keywords": [], 11 | "author": "Tomasz Jezierski, Adrian Kowalczewski", 12 | "license": "ISC", 13 | "dependencies": { 14 | "es6-promise": "^4.1.0", 15 | "es6-promise-promise": "^1.0.0", 16 | "history": "^4.6.1", 17 | "mobx": "^3.1.9", 18 | "mobx-preact": "^1.1.0", 19 | "preact": "^7.2.1", 20 | "preact-compat": "^3.14.3", 21 | "react-router-dom": "^4.0.0", 22 | "whatwg-fetch": "^2.0.3" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.24.0", 26 | "babel-eslint": "^6.1.2", 27 | "babel-loader": "^6.4.1", 28 | "babel-plugin-transform-class-properties": "^6.23.0", 29 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 30 | "babel-plugin-transform-react-jsx": "^6.23.0", 31 | "babel-preset-es2015": "^6.24.0", 32 | "babel-preset-react": "^6.23.0", 33 | "babel-preset-stage-2": "^6.24.1", 34 | "eslint": "^3.19.0", 35 | "eslint-config-airbnb": "^15.0.1", 36 | "eslint-loader": "^1.7.1", 37 | "eslint-plugin-import": "^2.2.0", 38 | "eslint-plugin-jsx-a11y": "^5.0.1", 39 | "eslint-plugin-react": "^7.0.1", 40 | "underscore": "^1.8.3", 41 | "webpack": "^2.3.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Api/Data/StatesInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 20 | 27 | 28 | -------------------------------------------------------------------------------- /Api/StatesRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | { 9 | options = options || {}; 10 | const apiKey = options.apiKey; 11 | const libraries = options.libraries || ['places', 'geometry']; 12 | const version = options.version || '3'; 13 | return ScriptCache({ 14 | google: GoogleApi({ apiKey: apiKey, libraries: libraries, version: version }), 15 | }); 16 | }; 17 | 18 | export const wrapper = (options = {}) => WrappedComponent => { 19 | const apiKey = options.apiKey; 20 | const libraries = ['places', 'geometry', 'drawing']; 21 | const version = options.version || '3'; 22 | const createCache = options.createCache || defaultCreateCache; 23 | 24 | class Wrapper extends Component { 25 | constructor(props, context) { 26 | super(props, context); 27 | options.apiKey = props.json.constants.apiKey; 28 | this.scriptCache = createCache(options); 29 | this.scriptCache.google.onLoad(this.onLoad.bind(this)); 30 | 31 | this.state = { 32 | loaded: false, 33 | map: null, 34 | google: null, 35 | }; 36 | } 37 | 38 | onLoad(err, tag) { 39 | this._gapi = window.google; 40 | 41 | this.setState({ loaded: true, google: this._gapi }); 42 | } 43 | 44 | render() { 45 | const props = Object.assign({}, this.props, { 46 | loaded: this.state.loaded, 47 | google: window.google, 48 | }); 49 | 50 | return ( 51 |
    52 | 53 |
    54 | ); 55 | } 56 | } 57 | 58 | return Wrapper; 59 | }; 60 | 61 | export default wrapper; 62 | -------------------------------------------------------------------------------- /view/frontend/preact/src/container/Directions.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { connect } from 'mobx-preact'; 3 | 4 | @connect(['stateStore']) 5 | export default class Directions extends Component { 6 | constructor(props, context) { 7 | super(props, context); 8 | this.directionsService = null; 9 | this.directionsDisplay = null; 10 | } 11 | 12 | getChildContext() { 13 | return { error: this.state.error }; 14 | } 15 | 16 | componentDidUpdate(prevProps) { 17 | if (this.props.points !== prevProps.points) { 18 | this.renderDirections(); 19 | } 20 | if (this.props.stateStore.view == 'list' && this.directionsDisplay) { 21 | this.directionsDisplay.setMap(null); 22 | } 23 | } 24 | 25 | renderDirections() { 26 | if (this.directionsService === null || this.directionsDisplay === null) { 27 | this.directionsService = new this.props.google.maps.DirectionsService(); 28 | this.directionsDisplay = new this.props.google.maps.DirectionsRenderer(); 29 | } 30 | 31 | this.directionsDisplay.setMap(this.props.map); 32 | const request = this.props.stateStore.getWaypoints; 33 | 34 | if (request.destination && request.origin) { 35 | this.directionsService.route(request, (result, status) => { 36 | if (status === 'OK') { 37 | this.props.stateStore.setError(); 38 | this.directionsDisplay.setDirections(result); 39 | const dirDiv = document.createElement('div'); 40 | const dirPanel = document.querySelector('#dirPanel'); 41 | dirPanel.appendChild(dirDiv); 42 | this.directionsDisplay.setPanel(dirDiv); 43 | } else { 44 | this.props.stateStore.setError('Could not find a route between A and B.'); 45 | } 46 | }); 47 | } 48 | } 49 | 50 | render() { 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Controller/Router.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class Router implements \Magento\Framework\App\RouterInterface 9 | { 10 | /** @var \Magento\Framework\App\ActionFactory */ 11 | protected $actionFactory; 12 | 13 | /** @var \Magento\Framework\App\ResponseInterface */ 14 | protected $_response; 15 | 16 | /** 17 | * Router constructor. 18 | * 19 | * @param \Magento\Framework\App\ActionFactory $actionFactory 20 | * @param \Magento\Framework\App\ResponseInterface $response 21 | */ 22 | public function __construct( 23 | \Magento\Framework\App\ActionFactory $actionFactory, 24 | \Magento\Framework\App\ResponseInterface $response 25 | ) { 26 | $this->actionFactory = $actionFactory; 27 | $this->_response = $response; 28 | } 29 | 30 | /** 31 | * Validate and Match 32 | * 33 | * @param \Magento\Framework\App\RequestInterface $request 34 | * @return bool 35 | */ 36 | public function match(\Magento\Framework\App\RequestInterface $request) 37 | { 38 | $identifier = trim($request->getPathInfo(), '/'); 39 | 40 | if(strpos($identifier, 'storelocator') !== false) { 41 | $request->setModuleName('storelocator')->setControllerName('index')->setActionName('index'); 42 | } else { 43 | return; 44 | } 45 | 46 | /* 47 | * We have match and now we will forward action 48 | */ 49 | return $this->actionFactory->create( 50 | 'Magento\Framework\App\Action\Forward', 51 | ['request' => $request] 52 | ); 53 | } 54 | } -------------------------------------------------------------------------------- /Controller/Adminhtml/Index/Index.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 26 | } 27 | 28 | public function execute() 29 | { 30 | /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ 31 | $resultPage = $this->resultPageFactory->create(); 32 | $resultPage->setActiveMenu('Magento_Backend::system_store'); 33 | $resultPage->addBreadcrumb(__('System'), __('System')); 34 | $resultPage->addBreadcrumb(__('Store Locator'), __('Manage Stores')); 35 | $resultPage->getConfig()->getTitle()->prepend(__('Manage Stores')); 36 | return $resultPage; 37 | } 38 | 39 | /** 40 | * Check Grid List Permission. 41 | * 42 | * @return bool 43 | */ 44 | protected function _isAllowed() 45 | { 46 | return $this->_authorization->isAllowed('PandaGroup_StoreLocator::storelocator'); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Regions/Index.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 26 | } 27 | 28 | public function execute() 29 | { 30 | /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ 31 | $resultPage = $this->resultPageFactory->create(); 32 | $resultPage->setActiveMenu('Magento_Backend::system_store'); 33 | $resultPage->addBreadcrumb(__('System'), __('System')); 34 | $resultPage->addBreadcrumb(__('Store Locator'), __('Manage Regions')); 35 | $resultPage->getConfig()->getTitle()->prepend(__('Manage Regions')); 36 | return $resultPage; 37 | } 38 | 39 | /** 40 | * Check Grid List Permission. 41 | * 42 | * @return bool 43 | */ 44 | protected function _isAllowed() 45 | { 46 | return $this->_authorization->isAllowed('PandaGroup_StoreLocator::storelocator'); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Setup/Uninstall.php: -------------------------------------------------------------------------------- 1 | startSetup(); 14 | 15 | // /** 16 | // * Delete table 'storelocator_image' 17 | // */ 18 | // $setup->getConnection()->dropTable('storelocator_image'); 19 | // 20 | // /** 21 | // * Delete table 'storelocator_value' 22 | // */ 23 | // $setup->getConnection()->dropTable('storelocator_value'); 24 | // 25 | // /** 26 | // * Delete table 'storelocator_tag' 27 | // */ 28 | // $setup->getConnection()->dropTable('storelocator_tag'); 29 | // 30 | // /** 31 | // * Delete table 'storelocator_specialday' 32 | // */ 33 | // $setup->getConnection()->dropTable('storelocator_specialday'); 34 | // 35 | // /** 36 | // * Delete table 'storelocator_holiday' 37 | // */ 38 | // $setup->getConnection()->dropTable('storelocator_holiday'); 39 | 40 | /** 41 | * Delete table 'storelocator' 42 | */ 43 | $setup->getConnection()->dropTable('storelocator'); 44 | 45 | /** 46 | * Delete table 'storelocator_states' 47 | */ 48 | $setup->getConnection()->dropTable('storelocator_states'); 49 | 50 | /** 51 | * Delete table 'storelocator_data_countries' 52 | */ 53 | $setup->getConnection()->dropTable('storelocator_data_countries'); 54 | 55 | /** 56 | * Delete table 'storelocator_data_regions' 57 | */ 58 | $setup->getConnection()->dropTable('storelocator_data_regions'); 59 | 60 | $setup->endSetup(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Model/Config/Source/ListCountry.php: -------------------------------------------------------------------------------- 1 | _catalogConfig = $catalogConfig; 29 | $this->countriesData = $countriesData; 30 | } 31 | 32 | /** 33 | * Retrieve option values array 34 | * 35 | * @return array 36 | */ 37 | public function toOptionArray() 38 | { 39 | $options = []; 40 | $options[] = ['label' => __(' '), 'value' => ' ']; 41 | foreach ($this->getCountryCodesAsArray() as $countryName => $countryCode) { 42 | $options[] = ['label' => __($countryName), 'value' => $countryCode]; 43 | } 44 | return $options; 45 | } 46 | 47 | /** 48 | * Retrieve Catalog Config Singleton 49 | * 50 | * @return \Magento\Catalog\Model\Config 51 | */ 52 | protected function _getCatalogConfig() 53 | { 54 | return $this->_catalogConfig; 55 | } 56 | 57 | private function getCountryCodesAsArray() 58 | { 59 | $countries = $this->countriesData->getCollection()->getData(); 60 | 61 | $countryCodes = []; 62 | foreach ($countries as $country) { 63 | $countryCodes[$country['name']] = strtoupper($country['code']); 64 | } 65 | 66 | return $countryCodes; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /view/frontend/web/css/modules/storelocator/_stores-li.scss: -------------------------------------------------------------------------------- 1 | .stores-li { 2 | display: flex; 3 | flex-wrap: wrap; 4 | max-width: 1120px; 5 | 6 | margin: 0 auto; 7 | 8 | &__store { 9 | flex: 0 1 25%; 10 | box-sizing: border-box; 11 | padding: 50px 10px 40px 30px; 12 | min-width: 250px; 13 | border-bottom: 1px solid; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: space-between; 17 | } 18 | 19 | &__name { 20 | font-size: 24px; 21 | font-family: Roboto, sans-serif; 22 | font-weight: 500; 23 | padding: 14px 0 20px 0; 24 | cursor: pointer; 25 | margin: 0; 26 | } 27 | 28 | &__credentials { 29 | font-size: 16px; 30 | width: 250px; 31 | max-width: 100%; 32 | 33 | li { 34 | margin: 4px 0 0 0; 35 | display: flex; 36 | align-items: center; 37 | line-height: 1.2em; 38 | margin-bottom: 8px; 39 | word-break: break-word; 40 | 41 | a { 42 | max-width: 100%; 43 | } 44 | 45 | &:first-child { 46 | font-weight: 500; 47 | } 48 | } 49 | 50 | 51 | } 52 | 53 | &__hours-wrapper { 54 | @include form-element-wrapper(); 55 | } 56 | 57 | &__hours { 58 | @include form-element(); 59 | padding: 0 18px 0 8px; 60 | } 61 | 62 | &__hours-arrow { 63 | @include form-select-wide-arrow(); 64 | } 65 | } 66 | 67 | @media screen and (max-width: $lg-breakpoint) { 68 | .stores-li { 69 | max-width: 750px; 70 | 71 | &__store { 72 | flex: 0 1 33%; 73 | } 74 | } 75 | } 76 | 77 | @media screen and (max-width: $sm-breakpoint) { 78 | .stores-li { 79 | 80 | &__store { 81 | flex: 0 1 50%; 82 | } 83 | } 84 | } 85 | 86 | @media screen and (max-width: 580px) { 87 | .stores-li { 88 | 89 | &__store { 90 | flex: 1 1 100%; 91 | } 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /Controller/Index/Json.php: -------------------------------------------------------------------------------- 1 | resultJsonFactory = $resultJsonFactory; 26 | $this->storeLocatorModel = $storeLocatorModel; 27 | parent::__construct($context); 28 | } 29 | 30 | /** 31 | * Blog Index, shows a list of recent blog posts. 32 | * 33 | * @return \Magento\Framework\Controller\Result\Json 34 | */ 35 | public function execute() 36 | { 37 | $result = $this->resultJsonFactory->create(); 38 | 39 | // if (isset($_SERVER['REMOTE_ADDR']) AND ($_SERVER['REMOTE_ADDR'] !== $_SERVER['SERVER_ADDR'])) { 40 | // 41 | // $response = [ 42 | // 'error' => '401', 43 | // 'message' => 'Invalid address. No access' 44 | // ]; 45 | // 46 | // $result->setData($response); 47 | // return $result; 48 | // } 49 | 50 | $result->setHeader('Cache-Control', 'max-age=86400, public, s-maxage=86400', true); 51 | $result->setHeader('X-Magento-Cache-Control', 'max-age=86400, public, s-maxage=86400', true); 52 | 53 | $response = $this->storeLocatorModel->getStoresData(); 54 | 55 | $result->setData($response); 56 | return $result; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /view/frontend/web/css/modules/storelocator/_directionstab.scss: -------------------------------------------------------------------------------- 1 | .DirectionsTab { 2 | &__radio-label { 3 | display: inline-block; 4 | background-color: $grey; 5 | color: white; 6 | padding: 7px 15px; 7 | margin: 0 7px 7px 0; 8 | 9 | input { 10 | display: none; 11 | } 12 | 13 | &--active { 14 | background-color: $black; 15 | } 16 | 17 | &--driving { 18 | 19 | } 20 | 21 | &--transit { 22 | 23 | } 24 | 25 | &--walking { 26 | 27 | } 28 | 29 | } 30 | 31 | &__directions-flexbox { 32 | display: flex; 33 | align-items: center; 34 | } 35 | 36 | &__directions-wrapper { 37 | flex: 1; 38 | display: flex; 39 | flex-direction: column; 40 | 41 | div { 42 | display: flex; 43 | height: 38px; 44 | margin: 5px 0; 45 | } 46 | } 47 | 48 | 49 | &__input-label { 50 | background-color: $black; 51 | color: white; 52 | width: 38px; 53 | line-height: 38px; 54 | text-align: center; 55 | font-size: 25px; 56 | font-weight: bold; 57 | border-top-left-radius: 4px; 58 | border-bottom-left-radius: 4px; 59 | } 60 | 61 | &__input-field { 62 | flex: 1; 63 | color: #676767; 64 | background-color: #efefef; 65 | border: none; 66 | padding: 0 10px; 67 | text-transform: uppercase; 68 | border-top-right-radius: 4px; 69 | border-bottom-right-radius: 4px; 70 | } 71 | 72 | &__input-button { 73 | display: block; 74 | color: white; 75 | background-color: $grey; 76 | border: none; 77 | height: 38px; 78 | border-radius: 4px; 79 | transition: all 0.3s ease-in-out 0s; 80 | 81 | &:hover { 82 | background-color: $black; 83 | } 84 | 85 | &--submit { 86 | padding: 0 50px; 87 | margin: 10px 0; 88 | } 89 | 90 | &--swap { 91 | margin-left: 10px; 92 | } 93 | 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /Model/States/DataProvider.php: -------------------------------------------------------------------------------- 1 | collection = $statesCollectionFactory->create(); 34 | $this->dataPersistor = $dataPersistor; 35 | } 36 | 37 | /** 38 | * Get data 39 | * 40 | * @return array 41 | */ 42 | public function getData() 43 | { 44 | if (isset($this->loadedData)) { 45 | return $this->loadedData; 46 | } 47 | 48 | $items = $this->collection->getItems(); 49 | /** @var \PandaGroup\StoreLocator\Model\States $state */ 50 | foreach ($items as $state) { 51 | $this->loadedData[$state->getId()] = $state->getData(); 52 | } 53 | 54 | $data = $this->dataPersistor->get('states_data'); 55 | if (!empty($data)) { 56 | $state = $this->collection->getNewEmptyItem(); 57 | $state->setData($data); 58 | $this->loadedData[$state->getId()] = $state->getData(); 59 | $this->dataPersistor->clear('states_data'); 60 | } 61 | 62 | return $this->loadedData; 63 | } 64 | } -------------------------------------------------------------------------------- /Model/ResourceModel/StoreLocator/Collection.php: -------------------------------------------------------------------------------- 1 | _init( 13 | 'PandaGroup\StoreLocator\Model\StoreLocator', 14 | 'PandaGroup\StoreLocator\Model\ResourceModel\StoreLocator' 15 | ); 16 | } 17 | 18 | /** 19 | * Define model & resource model 20 | */ 21 | const YOUR_TABLE = 'tablename'; 22 | 23 | public function __construct( 24 | \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory, 25 | \Psr\Log\LoggerInterface $logger, 26 | \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, 27 | \Magento\Framework\Event\ManagerInterface $eventManager, 28 | \Magento\Store\Model\StoreManagerInterface $storeManager, 29 | \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, 30 | \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null 31 | ) { 32 | $this->_init( 33 | 'PandaGroup\StoreLocator\Model\StoreLocator', 34 | 'PandaGroup\StoreLocator\Model\ResourceModel\StoreLocator' 35 | ); 36 | parent::__construct( 37 | $entityFactory, $logger, $fetchStrategy, $eventManager, $connection, 38 | $resource 39 | ); 40 | $this->storeManager = $storeManager; 41 | } 42 | 43 | protected function _initSelect() 44 | { 45 | parent::_initSelect(); 46 | 47 | $this->getSelect()->joinLeft( 48 | ['secondTable' => $this->getTable('storelocator_states')], 49 | 'main_table.state_id = secondTable.state_id', 50 | [ 51 | 'state_source_id', 52 | 'state_name', 53 | 'state_short_name', 54 | 'state_latitude' => 'latitude', 55 | 'state_longtitude' => 'longtitude', 56 | 'state_zoom_level' => 'zoom_level' 57 | ] 58 | ); 59 | } 60 | } -------------------------------------------------------------------------------- /Model/StoreLocator/DataProvider.php: -------------------------------------------------------------------------------- 1 | collection = $storeLocatorCollectionFactory->create(); 34 | $this->dataPersistor = $dataPersistor; 35 | } 36 | 37 | /** 38 | * Get data 39 | * 40 | * @return array 41 | */ 42 | public function getData() 43 | { 44 | if (isset($this->loadedData)) { 45 | return $this->loadedData; 46 | } 47 | 48 | $items = $this->collection->getItems(); 49 | /** @var \PandaGroup\StoreLocator\Model\StoreLocator $store */ 50 | foreach ($items as $store) { 51 | $this->loadedData[$store->getId()] = $store->getData(); 52 | } 53 | 54 | $data = $this->dataPersistor->get('storelocator'); 55 | if (!empty($data)) { 56 | $store = $this->collection->getNewEmptyItem(); 57 | $store->setData($data); 58 | $this->loadedData[$store->getId()] = $store->getData(); 59 | $this->dataPersistor->clear('storelocator'); 60 | } 61 | 62 | return $this->loadedData; 63 | } 64 | } -------------------------------------------------------------------------------- /view/frontend/preact/src/App.js: -------------------------------------------------------------------------------- 1 | import 'preact/devtools'; 2 | import { h, Component } from 'preact'; 3 | import { Provider } from 'mobx-preact'; 4 | import { useStrict } from 'mobx'; 5 | 6 | import { Route, BrowserRouter } from 'react-router-dom'; 7 | import createBrowserHistory from 'history/createBrowserHistory'; 8 | 9 | import Directions from './container/Directions'; 10 | import StateStore from './store/'; 11 | import StoreHeader from './container/StoreHeader'; 12 | import StoresList from './container/StoresList'; 13 | import StoreView from './container/StoreView'; 14 | 15 | import GoogleApiComponent from './component/GoogleApiComponent'; 16 | 17 | const history = createBrowserHistory(); 18 | 19 | class App extends Component { 20 | constructor(props) { 21 | super(props); 22 | useStrict(true); 23 | this.stateStore = new StateStore(props.json); 24 | } 25 | 26 | getChildContext() { 27 | return { 28 | constants: this.props.json.constants, 29 | google: this.props.google, 30 | }; 31 | } 32 | 33 | render() { 34 | const { json } = this.props; 35 | return ( 36 | 37 | 38 |
    39 | 40 | } 44 | /> 45 | ( 48 | 52 | )} 53 | /> 54 |
    55 |
    56 |
    57 | ); 58 | } 59 | } 60 | 61 | export default GoogleApiComponent()(App); 62 | -------------------------------------------------------------------------------- /view/frontend/preact/src/container/HeaderMap.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { connect } from 'mobx-preact'; 3 | import Map from '../component/Map'; 4 | 5 | import Directions from './Directions'; 6 | import Marker from '../component/Marker'; 7 | 8 | @connect(['stateStore']) 9 | export default class HeaderMap extends Component { 10 | constructor() { 11 | super(); 12 | this.markerClick = this.markerClick.bind(this); 13 | } 14 | 15 | markerClick(props) { 16 | this.props.stateStore.changeView('single'); 17 | this.context.router.history.push( 18 | `/${(props.addr_cty + '/' + props.name) 19 | .split(' ') 20 | .join('-') 21 | .toLowerCase()}` 22 | ); 23 | } 24 | 25 | render() { 26 | return ( 27 |
    30 | 40 | {this.props.stateStore.stores.map(store => ( 41 | 50 | ))} 51 | 52 | 53 |
    54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Model/Config/Source/ListZoom.php: -------------------------------------------------------------------------------- 1 | _catalogConfig = $catalogConfig; 22 | } 23 | 24 | /** 25 | * Retrieve option values array 26 | * 27 | * @return array 28 | */ 29 | public function toOptionArray() 30 | { 31 | $options = []; 32 | $options[] = ['label' => __(' '), 'value' => ' ']; 33 | foreach ($this->getZoomLevelsAsArray() as $zoomLevel => $zoomLevelLabel) { 34 | $options[] = ['label' => __($zoomLevelLabel), 'value' => $zoomLevel]; 35 | } 36 | return $options; 37 | } 38 | 39 | /** 40 | * Retrieve Catalog Config Singleton 41 | * 42 | * @return \Magento\Catalog\Model\Config 43 | */ 44 | protected function _getCatalogConfig() 45 | { 46 | return $this->_catalogConfig; 47 | } 48 | 49 | private function getZoomLevelsAsArray() 50 | { 51 | $zoomLevels = []; 52 | for ($i=1; $i<=20; $i++) { 53 | switch ($i) { 54 | case 1: 55 | $zoomLevels[$i] = $i . ' (World)'; 56 | break; 57 | 58 | case 5: 59 | $zoomLevels[$i] = $i . ' (Landmass/continent)'; 60 | break; 61 | 62 | case 10: 63 | $zoomLevels[$i] = $i . ' (City)'; 64 | break; 65 | 66 | case 15: 67 | $zoomLevels[$i] = $i . ' (Streets)'; 68 | break; 69 | 70 | case 20: 71 | $zoomLevels[$i] = $i . ' (Buildings)'; 72 | break; 73 | 74 | default: $zoomLevels[$i] = (string) $i; 75 | } 76 | } 77 | 78 | return $zoomLevels; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/fetchCoords.js: -------------------------------------------------------------------------------- 1 | define(['jquery'], function ($) { 2 | 'use strict'; 3 | 4 | return function () { 5 | var $lat = $('.admin__control-text[name="latitude"]'); 6 | var $lng = $('.admin__control-text[name="longtitude"]'); 7 | $lat.css({transition: "border 1s"}); 8 | $lng.css({transition: "border 1s"}); 9 | var $zoom = $('.admin__control-select[name="zoom_level"]'); 10 | var $cntry = $('.admin__control-select[name="country"]'); 11 | var $cntrytext = $('option[value=' + $cntry.val() + ']').text(); 12 | var $state = $('.admin__control-text[name="state_name"]'); 13 | var $cty = $('.admin__control-text[name="city"]'); 14 | var $addr = $('.admin__control-text[name="address"]'); 15 | var $message = $('#google-api-message'); 16 | if (!$message.length) { 17 | $('button[data-index="google-coords-fetch"]').after(''); 18 | $message = $($message.selector); 19 | } 20 | if ($zoom.val() === " ") { 21 | $zoom.val("15").trigger("change"); 22 | } 23 | var address = [$cntrytext, $state.val(), $cty.val(), $addr.val()]; 24 | $.ajax({ 25 | url: 'http://maps.google.com/maps/api/geocode/json', 26 | context: this, 27 | data: {address: address.join(', ')}, 28 | method: 'get' 29 | }).done(function(json) { 30 | if (json.status == 'OK') { 31 | $message.text(' Successfully fetched coordinates for address: ' + json.results[0].formatted_address); 32 | $lat.val(json.results[0].geometry.location.lat).css({border: "1px solid #10bf10"}).trigger("change"); 33 | $lng.val(json.results[0].geometry.location.lng).css({border: "1px solid #10bf10"}).trigger("change"); 34 | } else { 35 | $message.text(' Google Api Error! Cannot find such place! Check your Address!'); 36 | } 37 | }).fail(function() { 38 | $message.text(' Internal Error! Check your internet connection!'); 39 | }); 40 | } 41 | }); 42 | 43 | 44 | -------------------------------------------------------------------------------- /view/adminhtml/templates/system/config/coordinates_button.phtml: -------------------------------------------------------------------------------- 1 | helper('Magento\Framework\Json\Helper\Data'); 10 | ?> 11 | 12 | 46 | 47 | 48 | getButtonHtml() ?> 49 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Index/Delete.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 23 | parent::__construct($context); 24 | } 25 | 26 | /** 27 | * Delete action 28 | * 29 | * @return \Magento\Framework\Controller\ResultInterface 30 | */ 31 | public function execute() 32 | { 33 | /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ 34 | $resultRedirect = $this->resultRedirectFactory->create(); 35 | 36 | $id = (int) $this->getRequest()->getParam('id'); 37 | if ($id) { 38 | $this->logger->info('Start deleting store.'); 39 | try { 40 | $model = $this->_objectManager->create('PandaGroup\StoreLocator\Model\StoreLocator'); 41 | $model->load($id); 42 | $model->delete(); 43 | 44 | $this->messageManager->addSuccessMessage(__('You deleted the store.')); 45 | $this->logger->info(' Store was successful deleted. Id='.$id); 46 | 47 | $this->logger->info('Finish deleting store.'); 48 | return $resultRedirect->setPath('*/*/'); 49 | } catch (\Exception $e) { 50 | $this->messageManager->addErrorMessage($e->getMessage()); 51 | $this->logger->error(' Error while deleting the store id=' . $id, $e->getMessage()); 52 | 53 | $this->logger->info('Finish deleting store.'); 54 | return $resultRedirect->setPath('*/*/edit', ['id' => $id]); 55 | } 56 | } 57 | 58 | $this->messageManager->addErrorMessage(__('We can\'t find a store to delete.')); 59 | 60 | return $resultRedirect->setPath('*/*/'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Index/Edit.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 29 | $this->coreRegistry = $registry; 30 | } 31 | 32 | public function execute() 33 | { 34 | $id = (int) $this->getRequest()->getParam('id'); 35 | $model = $this->_objectManager->create('PandaGroup\StoreLocator\Model\StoreLocator'); 36 | 37 | if ($id) { 38 | $model->load($id); 39 | 40 | if (!$model->getId()) { 41 | $this->messageManager->addErrorMessage(__('This store no longer exists.')); 42 | /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ 43 | $resultRedirect = $this->resultRedirectFactory->create(); 44 | return $resultRedirect->setPath('*/*/'); 45 | } 46 | } 47 | 48 | $this->coreRegistry->register('storelocator', $model); 49 | 50 | /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ 51 | $resultPage = $this->resultPageFactory->create(); 52 | 53 | $resultPage->addBreadcrumb(__('Edit Store'), __('New Store')); 54 | $resultPage->addBreadcrumb(__('Edit Store'), __('New Store')); 55 | 56 | $resultPage->setActiveMenu('Magento_Backend::system_store'); 57 | $resultPage->getConfig()->getTitle()->prepend(__('Manage Stores')); 58 | $resultPage->getConfig()->getTitle()->prepend($model->getId() ? $model->getTitle() : __('New Store')); 59 | 60 | return $resultPage; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Regions/Edit.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 29 | $this->coreRegistry = $registry; 30 | } 31 | 32 | public function execute() 33 | { 34 | $id = (int) $this->getRequest()->getParam('id'); 35 | $model = $this->_objectManager->create('PandaGroup\StoreLocator\Model\States'); 36 | 37 | if ($id) { 38 | $model->load($id); 39 | 40 | if (!$model->getId()) { 41 | $this->messageManager->addErrorMessage(__('This region no longer exists.')); 42 | /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ 43 | $resultRedirect = $this->resultRedirectFactory->create(); 44 | return $resultRedirect->setPath('*/*/'); 45 | } 46 | } 47 | 48 | $this->coreRegistry->register('states_data', $model); 49 | 50 | /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ 51 | $resultPage = $this->resultPageFactory->create(); 52 | 53 | $resultPage->addBreadcrumb(__('Edit Region'), __('New Region')); 54 | $resultPage->addBreadcrumb(__('Edit Region'), __('New Region')); 55 | 56 | $resultPage->setActiveMenu('Magento_Backend::system_store'); 57 | $resultPage->getConfig()->getTitle()->prepend(__('Manage Regions')); 58 | $resultPage->getConfig()->getTitle()->prepend($model->getId() ? $model->getTitle() : __('New Region')); 59 | 60 | return $resultPage; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Block/Adminhtml/System/Config/ButtonCoordinates.php: -------------------------------------------------------------------------------- 1 | jsonHelper = $jsonHelper; 21 | parent::__construct($context, $data); 22 | } 23 | 24 | /** 25 | * Set template 26 | * 27 | * @return void 28 | */ 29 | protected function _construct() 30 | { 31 | parent::_construct(); 32 | $this->setTemplate('PandaGroup_StoreLocator::system/config/coordinates_button.phtml'); 33 | } 34 | 35 | /** 36 | * Generate button html 37 | * 38 | * @return string 39 | */ 40 | public function getButtonHtml() 41 | { 42 | $button = $this->getLayout()->createBlock( 43 | 'Magento\Backend\Block\Widget\Button' 44 | )->setData( 45 | [ 46 | 'id' => 'pandagroup_set_coordinates_by_country_button', 47 | 'label' => __('Get Coordinates for Country'), 48 | 'onclick' => 'javascript:fetchCountryCoords()', 49 | ] 50 | ); 51 | 52 | return $button->toHtml(); 53 | } 54 | 55 | /** 56 | * Render button 57 | * 58 | * @param \Magento\Framework\Data\Form\Element\AbstractElement $element 59 | * @return string 60 | */ 61 | public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element) 62 | { 63 | // Remove scope label 64 | $element->unsScope()->unsCanUseWebsiteValue()->unsCanUseDefaultValue(); 65 | return parent::render($element); 66 | } 67 | 68 | /** 69 | * Return element html 70 | * 71 | * @param \Magento\Framework\Data\Form\Element\AbstractElement $element 72 | * @return string 73 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 74 | */ 75 | protected function _getElementHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element) 76 | { 77 | return $this->_toHtml(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /view/frontend/preact/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { observable, action, computed } from 'mobx'; 2 | 3 | class StateStore { 4 | @observable filters = []; 5 | @observable stores = []; 6 | @observable geo; 7 | @observable zoom; 8 | @observable waypoints; 9 | @observable view = 'list'; 10 | @observable error = ''; 11 | 12 | constructor(json) { 13 | this.json = json; 14 | this.geo = json.constants.geo; 15 | this.zoom = json.constants.zoom; 16 | this.stores = json.stores; 17 | this.waypoints = { start: '', stop: '', mode: 'DRIVING' }; 18 | } 19 | 20 | @action 21 | initializeStore(router) { 22 | let { pathname, hash } = router.route.location; 23 | if (hash !== '') { 24 | hash = hash 25 | .replace('#', '') 26 | .replace('/', '') 27 | .toUpperCase(); 28 | this.addFilters(hash); 29 | } else if (pathname !== '/') { 30 | pathname = pathname.replace('/', ''); 31 | const store = this.json.stores.filter( 32 | store => 33 | (store.addr_cty + '/' + store.name) 34 | .split(' ') 35 | .join('-') 36 | .toLowerCase() == pathname 37 | )[0]; 38 | this.changeMap(store.geo, store.zoom); 39 | this.changeView(); 40 | } 41 | } 42 | 43 | @action 44 | clearFilters() { 45 | this.geo = this.json.constants.geo; 46 | this.zoom = this.json.constants.zoom; 47 | this.stores = this.json.stores; 48 | this.filters = []; 49 | this.error = ''; 50 | } 51 | 52 | @action 53 | addFilters(filter) { 54 | this.filters = []; 55 | const newRegion = this.json.regions.filter(region => region.name == filter)[0]; 56 | this.stores = this.json.stores.filter(store => store.region == filter); 57 | this.geo = newRegion.geo; 58 | this.zoom = newRegion.zoom; 59 | this.filters.push(filter); 60 | } 61 | 62 | @action 63 | changeMap(geo = this.json.constants.geo, zoom = this.json.constants.zoom) { 64 | this.geo = geo; 65 | this.zoom = zoom; 66 | } 67 | 68 | @action 69 | changeView(view = null) { 70 | if (!view) { 71 | this.view = this.view === 'list' ? 'single' : 'list'; 72 | } else { 73 | this.view = view; 74 | } 75 | this.error = ''; 76 | } 77 | 78 | @action 79 | updateWaypoints(start, stop, mode) { 80 | this.waypoints = { start: start, stop: stop, mode: mode }; 81 | } 82 | 83 | @action 84 | setError(error = '') { 85 | this.error = error; 86 | } 87 | 88 | @computed 89 | get geoTotal() { 90 | return { lat: this.geo.lat, lng: this.geo.lng }; 91 | } 92 | 93 | @computed 94 | get getWaypoints() { 95 | return { 96 | origin: this.waypoints.start, 97 | destination: this.waypoints.stop, 98 | travelMode: this.waypoints.mode, 99 | }; 100 | } 101 | } 102 | 103 | export default StateStore; 104 | -------------------------------------------------------------------------------- /Controller/Regions/GetByCountry.php: -------------------------------------------------------------------------------- 1 | resultJsonFactory = $resultJsonFactory; 26 | $this->listState = $listState; 27 | parent::__construct($context); 28 | } 29 | 30 | /** 31 | * Blog Index, shows a list of recent blog posts. 32 | * 33 | * @return \Magento\Framework\Controller\Result\Json 34 | */ 35 | public function execute() 36 | { 37 | $result = $this->resultJsonFactory->create(); 38 | 39 | // if (isset($_SERVER['REMOTE_ADDR']) AND ($_SERVER['REMOTE_ADDR'] !== $_SERVER['SERVER_ADDR'])) { 40 | // 41 | // $response = [ 42 | // 'error' => '401', 43 | // 'message' => 'Invalid address. No access' 44 | // ]; 45 | // 46 | // $result->setData($response); 47 | // return $result; 48 | // } 49 | 50 | $countryCode = $this->getRequest()->getParam('country'); 51 | 52 | $response = [ 53 | 'status' => 0, 54 | 'error' => __('Bad country code.') 55 | ]; 56 | 57 | if ($countryCode) { 58 | $states = $this->listState->getRegionsAsArray($countryCode); 59 | 60 | if (null !== $states) { 61 | if (false === empty($states)) { 62 | $response = [ 63 | 'status' => 1, 64 | 'states' => $states 65 | ]; 66 | 67 | $result->setData($response); 68 | return $result; 69 | } else { 70 | $regionsByCountry[1] = ''; 71 | 72 | $response = [ 73 | 'status' => 0, 74 | 'error' => __('Luck of states.') 75 | ]; 76 | } 77 | } 78 | 79 | } 80 | 81 | $result->setData($response); 82 | return $result; 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/fetchRegionsForCountry.js: -------------------------------------------------------------------------------- 1 | define(['jquery','Magento_Ui/js/form/element/select'], function ($, Select) { 2 | 'use strict'; 3 | 4 | return Select.extend({ 5 | initialize: function() { 6 | this._super(); 7 | this.timeout(); 8 | }, 9 | timeout: function() { 10 | var that = this; 11 | this.checkExist = setInterval(function() { 12 | if ($('.admin__control-select[name="country"]').length) { 13 | that.domReadyInits(); 14 | clearInterval(that.checkExist); 15 | } 16 | }, 200); 17 | }, 18 | domReadyInits: function() { 19 | this.$cntry = $('.admin__control-select[name="country"]'); 20 | this.$state = $('.admin__control-select[name="state_source_id"]'); 21 | this.initLoaders(); 22 | this.bindClick(); 23 | if (this.$cntry.val() !== " ") { 24 | this.updtSelect(); 25 | } 26 | }, 27 | initLoaders: function() { 28 | $(document).ajaxStart(function(){ 29 | $('.admin__form-loading-mask[data-role="spinner"]').show(); 30 | }); 31 | $(document).ajaxComplete(function(){ 32 | $('.admin__form-loading-mask[data-role="spinner"]').hide(); 33 | }); 34 | }, 35 | bindClick: function() { 36 | this.$cntry.click(this.updtSelect.bind(this)); 37 | }, 38 | updtSelect: function() { 39 | 40 | console.log(this.$state.val()); 41 | $.ajax({ 42 | url: '/storelocator/regions/getbycountry', 43 | context: this, 44 | data: {country: this.$cntry.val()}, 45 | method: 'get' 46 | }) 47 | .done(function(json) { 48 | if (json.status) { 49 | this.$state.empty(); 50 | for (var state in json.states) { 51 | if (json.states.hasOwnProperty(state)) { 52 | this.$state 53 | .append($("") 54 | .attr("value",state) 55 | .text(json.states[state])); 56 | } 57 | } 58 | var activeState = this.$state.val(); 59 | if (activeState !== ' ') { 60 | this.$state.val(activeState).trigger("change"); 61 | } 62 | } else { 63 | window.alert(json.error); 64 | } 65 | }) 66 | .fail(function() { 67 | window.alert('External error!'); 68 | }); 69 | } 70 | }); 71 | 72 | }); 73 | 74 | 75 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Regions/InlineEdit.php: -------------------------------------------------------------------------------- 1 | statesRepository = $statesRepository; 27 | $this->jsonFactory = $jsonFactory; 28 | } 29 | 30 | /** 31 | * @return \Magento\Framework\Controller\ResultInterface 32 | */ 33 | public function execute() 34 | { 35 | /** @var \Magento\Framework\Controller\Result\Json $resultJson */ 36 | $resultJson = $this->jsonFactory->create(); 37 | $error = false; 38 | $messages = []; 39 | 40 | if ($this->getRequest()->getParam('isAjax')) { 41 | $postItems = $this->getRequest()->getParam('items', []); 42 | if (!count($postItems)) { 43 | $messages[] = __('Please correct the data sent.'); 44 | $error = true; 45 | } else { 46 | foreach (array_keys($postItems) as $stateId) { 47 | /** @var \PandaGroup\StoreLocator\Model\States $state */ 48 | $state = $this->statesRepository->getById($stateId); 49 | try { 50 | $state->setData(array_merge($state->getData(), $postItems[$stateId])); 51 | $this->statesRepository->save($state); 52 | } catch (\Exception $e) { 53 | $messages[] = $this->getErrorWithStateId( 54 | $state, 55 | __($e->getMessage()) 56 | ); 57 | $error = true; 58 | } 59 | } 60 | } 61 | } 62 | 63 | return $resultJson->setData([ 64 | 'messages' => $messages, 65 | 'error' => $error 66 | ]); 67 | } 68 | 69 | /** 70 | * Add state title to error message 71 | * 72 | * @param \PandaGroup\StoreLocator\Api\Data\StatesInterface $state 73 | * @param $errorText 74 | * @return string 75 | */ 76 | protected function getErrorWithStateId(\PandaGroup\StoreLocator\Api\Data\StatesInterface $state, $errorText) 77 | { 78 | return '[STATE ID: ' . $state->getId() . '] ' . $errorText; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Ui/Component/Listing/Column/RegionsActions.php: -------------------------------------------------------------------------------- 1 | urlBuilder = $urlBuilder; 40 | parent::__construct($context, $uiComponentFactory, $components, $data); 41 | } 42 | 43 | /** 44 | * Prepare Data Source 45 | * 46 | * @param array $dataSource 47 | * 48 | * @return array 49 | */ 50 | public function prepareDataSource(array $dataSource) 51 | { 52 | if (isset($dataSource['data']['items'])) { 53 | foreach ($dataSource['data']['items'] as & $item) { 54 | if (isset($item['state_id'])) { 55 | $item[$this->getData('name')] = [ 56 | 'edit' => [ 57 | 'href' => $this->urlBuilder->getUrl( 58 | static::URL_PATH_EDIT, 59 | [ 60 | 'id' => $item['state_id'] 61 | ] 62 | ), 63 | 'label' => __('Edit') 64 | ], 65 | 'delete' => [ 66 | 'href' => $this->urlBuilder->getUrl( 67 | static::URL_PATH_DELETE, 68 | [ 69 | 'id' => $item['state_id'] 70 | ] 71 | ), 72 | 'label' => __('Delete'), 73 | 'confirm' => [ 74 | 'title' => __('Delete "${ $.$data.state_name }"'), 75 | 'message' => __('Are you sure you wan\'t to delete a "${ $.$data.state_name }" region?') 76 | ] 77 | ] 78 | ]; 79 | } 80 | } 81 | } 82 | 83 | return $dataSource; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Index/InlineEdit.php: -------------------------------------------------------------------------------- 1 | storeLocatorRepository = $storeLocatorRepository; 27 | $this->jsonFactory = $jsonFactory; 28 | } 29 | 30 | /** 31 | * @return \Magento\Framework\Controller\ResultInterface 32 | */ 33 | public function execute() 34 | { 35 | /** @var \Magento\Framework\Controller\Result\Json $resultJson */ 36 | $resultJson = $this->jsonFactory->create(); 37 | $error = false; 38 | $messages = []; 39 | 40 | if ($this->getRequest()->getParam('isAjax')) { 41 | $postItems = $this->getRequest()->getParam('items', []); 42 | if (!count($postItems)) { 43 | $messages[] = __('Please correct the data sent.'); 44 | $error = true; 45 | } else { 46 | foreach (array_keys($postItems) as $storeId) { 47 | /** @var \PandaGroup\StoreLocator\Model\StoreLocator $store */ 48 | $store = $this->storeLocatorRepository->getById($storeId); 49 | 50 | try { 51 | $store->setData(array_merge($store->getData(), $postItems[$storeId])); 52 | $this->storeLocatorRepository->save($store); 53 | } catch (\Exception $e) { 54 | $messages[] = $this->getErrorWithStoreLocatorId( 55 | $store, 56 | __($e->getMessage()) 57 | ); 58 | $error = true; 59 | } 60 | } 61 | } 62 | } 63 | 64 | return $resultJson->setData([ 65 | 'messages' => $messages, 66 | 'error' => $error 67 | ]); 68 | } 69 | 70 | /** 71 | * Add store title to error message 72 | * 73 | * @param \PandaGroup\StoreLocator\Api\Data\StoreLocatorInterface $store 74 | * @param $errorText 75 | * @return string 76 | */ 77 | protected function getErrorWithStoreLocatorId(\PandaGroup\StoreLocator\Api\Data\StoreLocatorInterface $store, $errorText) 78 | { 79 | return '[STORE ID: ' . $store->getId() . '] ' . $errorText; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Magento\Framework\Filesystem\Driver\File 9 | 10 | 11 | 12 | 13 | PandaGroup/StoreLocator 14 | 15 | PandaGroup\StoreLocator\Logger\Handler 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PandaGroup\StoreLocator\Model\ResourceModel\StoreLocator\Grid\Collection 29 | 30 | 31 | 32 | 33 | 34 | storelocator 35 | pandagroup_storelocator_grid_collection 36 | pandagroup_grid_collection 37 | PandaGroup\StoreLocator\Model\ResourceModel\StoreLocator 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | storelocator_states 47 | PandaGroup\StoreLocator\Model\ResourceModel\States 48 | 49 | 50 | 51 | 52 | 53 | PandaGroup\StoreLocator\Model\ResourceModel\States\Grid\Collection 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Api/Data/StoreLocatorInterface.php: -------------------------------------------------------------------------------- 1 | dataPersistor = $dataPersistor; 39 | $this->regionsData = $regionsData; 40 | $this->states = $states; 41 | $this->statesFactory = $statesFactory; 42 | parent::__construct($context); 43 | } 44 | 45 | public function execute() 46 | { 47 | /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ 48 | $resultRedirect = $this->resultRedirectFactory->create(); 49 | $data = $this->getRequest()->getPostValue(); 50 | if ($data) { 51 | $id = (int) $this->getRequest()->getParam('id'); 52 | 53 | $model = $this->_objectManager->create('PandaGroup\StoreLocator\Model\States')->load($id); 54 | if (!$model->getId() && $id) { 55 | $this->messageManager->addErrorMessage(__('This region no longer exists.')); 56 | 57 | return $resultRedirect->setPath('*/*/'); 58 | } 59 | 60 | $model->setData($data); 61 | 62 | try { 63 | $model->save(); 64 | $this->messageManager->addSuccessMessage(__('You saved the region.')); 65 | $this->dataPersistor->clear('states_data'); 66 | 67 | if ($this->getRequest()->getParam('back')) { 68 | return $resultRedirect->setPath('*/*/edit', ['id' => $model->getId()]); 69 | } 70 | 71 | return $resultRedirect->setPath('*/*/'); 72 | } catch (LocalizedException $e) { 73 | $this->messageManager->addErrorMessage($e->getMessage()); 74 | } catch (\Exception $e) { 75 | $this->messageManager->addExceptionMessage($e, __('Something went wrong while saving the region.')); 76 | } 77 | 78 | $this->dataPersistor->set('states_data', $data); 79 | 80 | return $resultRedirect->setPath('*/*/edit', ['state_id' => $this->getRequest()->getParam('state_id')]); 81 | } 82 | 83 | return $resultRedirect->setPath('*/*/'); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /view/frontend/web/css/modules/storelocator/_storelocator-header.scss: -------------------------------------------------------------------------------- 1 | .storelocator-header { 2 | letter-spacing: 1px; 3 | 4 | &__title { 5 | text-align: center; 6 | font-size: 40px; 7 | background: #f7f7f7; 8 | margin: 0; 9 | height: 163px; 10 | font-family: 'Roboto', sans-serif; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | font-weight: 500; 15 | } 16 | 17 | &__row { 18 | display: flex; 19 | justify-content: space-between; 20 | margin: 13px auto; 21 | padding: 0 13px; 22 | max-width: 960px; 23 | 24 | &--breadcrumbs { 25 | letter-spacing: 0; 26 | } 27 | } 28 | 29 | &__filters { 30 | display: flex; 31 | flex-wrap: wrap; 32 | flex: 1; 33 | } 34 | 35 | &__filter { 36 | font-weight: 400; 37 | font-size: 22px; 38 | margin-right: 20px; 39 | cursor: pointer; 40 | 41 | &--active { 42 | text-decoration: underline; 43 | } 44 | } 45 | 46 | &__reset { 47 | color: $grey-dark; 48 | text-decoration: underline; 49 | cursor: pointer; 50 | margin: 0 30px 0 0; 51 | font-size: 17px; 52 | } 53 | 54 | &__input { 55 | height: 38px; 56 | border: 1px solid #b9b9b9; 57 | background-color: white; 58 | box-sizing: border-box; 59 | padding: 0 22px; 60 | color: #515151; 61 | font-size: 14px; 62 | } 63 | 64 | &__back, &__search { 65 | padding: 10px 35px; 66 | font-size: 16px; 67 | font-weight: 400; 68 | transition: background 0.3s, color 0.3s ease-in-out; 69 | margin: 11px 0 0 12px; 70 | letter-spacing: 0; 71 | cursor: pointer; 72 | background: #363636; 73 | color: white; 74 | border: none; 75 | height: 40px; 76 | font-family: Roboto, arial, sans-serif; 77 | 78 | &:hover { 79 | background: #666; 80 | color: white; 81 | } 82 | } 83 | 84 | &__error { 85 | text-align: center; 86 | color: $red; 87 | } 88 | 89 | @media all and (max-width: 800px) { 90 | &__title { 91 | height: 123px; 92 | font-size: 24px; 93 | } 94 | 95 | &__back-a { 96 | padding: 5px; 97 | } 98 | 99 | &__back { 100 | margin: 10px 0; 101 | //width: 100%; 102 | } 103 | 104 | &__row { 105 | flex-direction: column; 106 | 107 | article:nth-child(2) { 108 | display: flex; 109 | flex-wrap: wrap; 110 | align-items: center; 111 | 112 | & > * { 113 | box-sizing: border-box; 114 | } 115 | } 116 | } 117 | 118 | &__reset { 119 | flex: 1 1 100%; 120 | text-align: center; 121 | } 122 | 123 | &__input { 124 | flex: 1; 125 | flex-basis: 300px; 126 | } 127 | 128 | &__search { 129 | flex: 1; 130 | flex-basis: 120px; 131 | 132 | } 133 | 134 | &__search, &__input, &__reset { 135 | margin: 5px; 136 | } 137 | } 138 | 139 | @media all and (max-width: $xs-breakpoint) { 140 | &__filters { 141 | justify-content: space-between; 142 | } 143 | 144 | &__filter { 145 | margin-right: 0; 146 | } 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /Model/ResourceModel/StoreLocator/Grid/Collection.php: -------------------------------------------------------------------------------- 1 | _eventPrefix = $eventPrefix; 44 | $this->_eventObject = $eventObject; 45 | $this->_init($model, $resourceModel); 46 | $this->setMainTable($mainTable); 47 | } 48 | 49 | /** 50 | * @return \Magento\Framework\Api\Search\AggregationInterface 51 | */ 52 | public function getAggregations() 53 | { 54 | return $this->aggregations; 55 | } 56 | 57 | /** 58 | * @param \Magento\Framework\Api\Search\AggregationInterface $aggregations 59 | * @return $this 60 | */ 61 | public function setAggregations($aggregations) 62 | { 63 | $this->aggregations = $aggregations; 64 | return $this; 65 | } 66 | 67 | /** 68 | * Get search criteria. 69 | * 70 | * @return \Magento\Framework\Api\SearchCriteriaInterface|null 71 | */ 72 | public function getSearchCriteria() 73 | { 74 | return null; 75 | } 76 | 77 | /** 78 | * Set search criteria. 79 | * 80 | * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria 81 | * 82 | * @return $this 83 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 84 | */ 85 | public function setSearchCriteria( 86 | \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria = null 87 | ) { 88 | return $this; 89 | } 90 | 91 | /** 92 | * Get total count. 93 | * 94 | * @return int 95 | */ 96 | public function getTotalCount() 97 | { 98 | return $this->getSize(); 99 | } 100 | 101 | /** 102 | * Set total count. 103 | * 104 | * @param int $totalCount 105 | * 106 | * @return $this 107 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 108 | */ 109 | public function setTotalCount($totalCount) 110 | { 111 | return $this; 112 | } 113 | 114 | /** 115 | * Set items list. 116 | * 117 | * @param \Magento\Framework\Api\ExtensibleDataInterface[] $items 118 | * 119 | * @return $this 120 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 121 | */ 122 | public function setItems(array $items = null) 123 | { 124 | return $this; 125 | } 126 | } -------------------------------------------------------------------------------- /Model/Config/Source/ListOpenHours.php: -------------------------------------------------------------------------------- 1 | _catalogConfig = $catalogConfig; 28 | $this->configProvider = $configProvider; 29 | } 30 | 31 | /** 32 | * Retrieve option values array 33 | * 34 | * @return array 35 | */ 36 | public function toOptionArray() 37 | { 38 | $options = []; 39 | $options[] = ['label' => __(' '), 'value' => ' ']; 40 | $timeFormat = $this->configProvider->getHoursTimeFormat(); 41 | foreach ($this->getHoursAsArray($timeFormat) as $timeLabel => $timeValue) { 42 | $options[] = ['label' => __($timeLabel), 'value' => $timeValue]; 43 | } 44 | return $options; 45 | } 46 | 47 | /** 48 | * Retrieve Catalog Config Singleton 49 | * 50 | * @return \Magento\Catalog\Model\Config 51 | */ 52 | protected function _getCatalogConfig() 53 | { 54 | return $this->_catalogConfig; 55 | } 56 | 57 | private function getHoursAsArray($format = 24) 58 | { 59 | if ($format == 12) { 60 | $hours = [ //12h format opening 61 | ' 0:00 AM' => '0:00', 62 | ' 0:30 AM' => '0:30', 63 | ' 1:00 AM' => '1:00', 64 | ' 1:30 AM' => '1:30', 65 | ' 2:00 AM' => '2:00', 66 | ' 2:30 AM' => '2:30', 67 | ' 3:00 AM' => '3:00', 68 | ' 3:30 AM' => '3:30', 69 | ' 4:00 AM' => '4:00', 70 | ' 4:30 AM' => '4:30', 71 | ' 5:00 AM' => '5:00', 72 | ' 5:30 AM' => '5:30', 73 | ' 6:00 AM' => '6:00', 74 | ' 6:30 AM' => '6:30', 75 | ' 7:00 AM' => '7:00', 76 | ' 7:30 AM' => '7:30', 77 | ' 8:00 AM' => '8:00', 78 | ' 8:30 AM' => '8:30', 79 | ' 9:00 AM' => '9:00', 80 | ' 9:30 AM' => '9:30', 81 | '10:00 AM' => '10:00', 82 | '10:30 AM' => '10:30', 83 | '11:00 AM' => '11:00', 84 | '11:30 AM' => '11:30', 85 | '12:00 AM' => '12:00' 86 | ]; 87 | } else { 88 | $hours = [ //24h format opening 89 | ' 0:00' => '0:00', 90 | ' 0:30' => '0:30', 91 | ' 1:00' => '1:00', 92 | ' 1:30' => '1:30', 93 | ' 2:00' => '2:00', 94 | ' 2:30' => '2:30', 95 | ' 3:00' => '3:00', 96 | ' 3:30' => '3:30', 97 | ' 4:00' => '4:00', 98 | ' 4:30' => '4:30', 99 | ' 5:00' => '5:00', 100 | ' 5:30' => '5:30', 101 | ' 6:00' => '6:00', 102 | ' 6:30' => '6:30', 103 | ' 7:00' => '7:00', 104 | ' 7:30' => '7:30', 105 | ' 8:00' => '8:00', 106 | ' 8:30' => '8:30', 107 | ' 9:00' => '9:00', 108 | ' 9:30' => '9:30', 109 | '10:00' => '10:00', 110 | '10:30' => '10:30', 111 | '11:00' => '11:00', 112 | '11:30' => '11:30', 113 | '12:00' => '12:00' 114 | ]; 115 | } 116 | 117 | return $hours; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Logger/Logger.php: -------------------------------------------------------------------------------- 1 | create('PandaGroup\StoreLocator\Helper\ConfigProvider'); 13 | return $configProvider->getDebugStatus(); 14 | } 15 | 16 | public function addDebug($message, array $context = array()) 17 | { 18 | if (false === $this->canLog()) return true; 19 | return $this->addRecord(static::DEBUG, $message, $context); 20 | } 21 | 22 | public function addInfo($message, array $context = array()) 23 | { 24 | if (false === $this->canLog()) return true; 25 | return $this->addRecord(static::INFO, $message, $context); 26 | } 27 | 28 | public function addNotice($message, array $context = array()) 29 | { 30 | return $this->addRecord(static::NOTICE, $message, $context); 31 | } 32 | 33 | public function addWarning($message, array $context = array()) 34 | { 35 | return $this->addRecord(static::WARNING, $message, $context); 36 | } 37 | 38 | public function addError($message, array $context = array()) 39 | { 40 | return $this->addRecord(static::ERROR, $message, $context); 41 | } 42 | 43 | public function addCritical($message, array $context = array()) 44 | { 45 | return $this->addRecord(static::CRITICAL, $message, $context); 46 | } 47 | 48 | public function addAlert($message, array $context = array()) 49 | { 50 | return $this->addRecord(static::ALERT, $message, $context); 51 | } 52 | 53 | public function addEmergency($message, array $context = array()) 54 | { 55 | return $this->addRecord(static::EMERGENCY, $message, $context); 56 | } 57 | 58 | public function debug($message, array $context = array()) 59 | { 60 | return $this->addRecord(static::DEBUG, $message, $context); 61 | } 62 | 63 | public function info($message, array $context = array()) 64 | { 65 | if (false === $this->canLog()) return true; 66 | return $this->addRecord(static::INFO, $message, $context); 67 | } 68 | 69 | public function notice($message, array $context = array()) 70 | { 71 | if (false === $this->canLog()) return true; 72 | return $this->addRecord(static::NOTICE, $message, $context); 73 | } 74 | 75 | public function warn($message, array $context = array()) 76 | { 77 | if (false === $this->canLog()) return true; 78 | return $this->addRecord(static::WARNING, $message, $context); 79 | } 80 | 81 | public function warning($message, array $context = array()) 82 | { 83 | if (false === $this->canLog()) return true; 84 | return $this->addRecord(static::WARNING, $message, $context); 85 | } 86 | 87 | public function err($message, array $context = array()) 88 | { 89 | return $this->addRecord(static::ERROR, $message, $context); 90 | } 91 | 92 | public function error($message, array $context = array()) 93 | { 94 | if (false === $this->canLog()) return true; 95 | return $this->addRecord(static::ERROR, $message, $context); 96 | } 97 | 98 | public function crit($message, array $context = array()) 99 | { 100 | return $this->addRecord(static::CRITICAL, $message, $context); 101 | } 102 | 103 | public function critical($message, array $context = array()) 104 | { 105 | return $this->addRecord(static::CRITICAL, $message, $context); 106 | } 107 | 108 | public function alert($message, array $context = array()) 109 | { 110 | return $this->addRecord(static::ALERT, $message, $context); 111 | } 112 | 113 | public function emerg($message, array $context = array()) 114 | { 115 | return $this->addRecord(static::EMERGENCY, $message, $context); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Model/Config/Source/ListCloseHours.php: -------------------------------------------------------------------------------- 1 | _catalogConfig = $catalogConfig; 29 | $this->configProvider = $configProvider; 30 | } 31 | 32 | /** 33 | * Retrieve option values array 34 | * 35 | * @return array 36 | */ 37 | public function toOptionArray() 38 | { 39 | $options = []; 40 | $options[] = ['label' => __(' '), 'value' => ' ']; 41 | $timeFormat = $this->configProvider->getHoursTimeFormat(); 42 | foreach ($this->getHoursAsArray($timeFormat) as $timeLabel => $timeValue) { 43 | $options[] = ['label' => __($timeLabel), 'value' => $timeValue]; 44 | } 45 | return $options; 46 | } 47 | 48 | /** 49 | * Retrieve Catalog Config Singleton 50 | * 51 | * @return \Magento\Catalog\Model\Config 52 | */ 53 | protected function _getCatalogConfig() 54 | { 55 | return $this->_catalogConfig; 56 | } 57 | 58 | private function getHoursAsArray($format = 24) 59 | { 60 | if ($format == 12) { 61 | $hours = [ //12h format closing 62 | '12:00 AM' => '12:00', 63 | '12:30 AM' => '12:30', 64 | ' 1:00 PM' => '13:00', 65 | ' 1:30 PM' => '13:30', 66 | ' 2:00 PM' => '14:00', 67 | ' 2:30 PM' => '14:30', 68 | ' 3:00 PM' => '15:00', 69 | ' 3:30 PM' => '15:30', 70 | ' 4:00 PM' => '16:00', 71 | ' 4:30 PM' => '16:30', 72 | ' 5:00 PM' => '17:00', 73 | ' 5:30 PM' => '17:30', 74 | ' 6:00 PM' => '18:00', 75 | ' 6:30 PM' => '18:30', 76 | ' 7:00 PM' => '19:00', 77 | ' 7:30 PM' => '19:30', 78 | ' 8:00 PM' => '20:00', 79 | ' 8:30 PM' => '20:30', 80 | ' 9:00 PM' => '21:00', 81 | ' 9:30 PM' => '21:30', 82 | '10:00 PM' => '22:00', 83 | '10:30 PM' => '22:30', 84 | '11:00 PM' => '23:00', 85 | '11:30 PM' => '23:30', 86 | '12:00 PM' => '0:00' 87 | ]; 88 | } else { 89 | $hours = [ //24h format closing 90 | '12:00' => '12:00', 91 | '12:30' => '12:30', 92 | '13:00' => '13:00', 93 | '13:30' => '13:30', 94 | '14:00' => '14:00', 95 | '14:30' => '14:30', 96 | '15:00' => '15:00', 97 | '15:30' => '15:30', 98 | '16:00' => '16:00', 99 | '16:30' => '16:30', 100 | '17:00' => '17:00', 101 | '17:30' => '17:30', 102 | '18:00' => '18:00', 103 | '18:30' => '18:30', 104 | '19:00' => '19:00', 105 | '19:30' => '19:30', 106 | '20:00' => '20:00', 107 | '20:30' => '20:30', 108 | '21:00' => '21:00', 109 | '21:30' => '21:30', 110 | '22:00' => '22:00', 111 | '22:30' => '22:30', 112 | '23:00' => '23:00', 113 | '23:30' => '23:30', 114 | '00:00' => '0:00' 115 | ]; 116 | } 117 | 118 | return $hours; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
    6 | pandagroup-store_locator 7 | 8 | panda_group 9 | PandaGroup_StoreLocator::config 10 | 11 | 12 | 13 | 14 | required-entry 15 | 16 | 17 | 18 | PandaGroup\StoreLocator\Model\Config\Source\ListCountry 19 | 20 | 21 | 22 | required-entry 23 | PandaGroup\StoreLocator\Model\Config\Source\ListTimeFormat 24 | 25 | 26 | 27 | 28 | 29 | 30 | required-entry 31 | 32 | 33 | 34 | required-entry 35 | 36 | 37 | 38 | PandaGroup\StoreLocator\Block\Adminhtml\System\Config\ButtonCoordinates 39 | 40 | 41 | 42 | required-entry 43 | PandaGroup\StoreLocator\Model\Config\Source\ListZoom 44 | 45 | 46 | 47 | PandaGroup\StoreLocator\Model\Config\Backend\Image 48 | storelocator 49 | 50 | 51 | 52 | 53 | 54 | 55 | Turns on logging to custom file. 56 | Magento\Config\Model\Config\Source\Yesno 57 | 58 | 59 |
    60 |
    61 |
    62 | -------------------------------------------------------------------------------- /Model/StatesRepository.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 29 | $this->statesFactory = $statesFactory; 30 | $this->collectionFactory = $collectionFactory; 31 | } 32 | 33 | /** 34 | * Save State 35 | * 36 | * @param \PandaGroup\StoreLocator\Api\Data\StatesInterface $state 37 | * @return \PandaGroup\StoreLocator\Api\Data\StatesInterface 38 | * @throws \Magento\Framework\Exception\CouldNotSaveException 39 | */ 40 | public function save(\PandaGroup\StoreLocator\Api\Data\StatesInterface $state) 41 | { 42 | try { 43 | $this->resource->save($state); 44 | } catch (\Exception $exception) { 45 | throw new \Magento\Framework\Exception\CouldNotSaveException(__($exception->getMessage())); 46 | } 47 | 48 | return $state; 49 | } 50 | 51 | /** 52 | * Retrieve State 53 | * 54 | * @param $stateId 55 | * @return \PandaGroup\StoreLocator\Model\States 56 | * @throws \Magento\Framework\Exception\NoSuchEntityException 57 | */ 58 | public function getById($stateId) 59 | { 60 | $state = $this->statesFactory->create(); 61 | $this->resource->load($state, $stateId); 62 | if (!$state->getId()) { 63 | throw new NoSuchEntityException(__('Region with id "%1" does not exist.', $stateId)); 64 | } 65 | 66 | return $state; 67 | } 68 | 69 | /** 70 | * Retrieve entity matching the specified criteria. 71 | * 72 | * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria 73 | * @return \PandaGroup\StoreLocator\Api\Data\StatesInterface[] 74 | */ 75 | public function getList(SearchCriteriaInterface $searchCriteria) 76 | { 77 | $collection = $this->collectionFactory->create(); 78 | foreach ($searchCriteria->getFilterGroups() as $filterGroup) { 79 | foreach ($filterGroup->getFilters() as $filter) { 80 | $condition = $filter->getConditionType() ?: 'eq'; 81 | $collection->addFieldToFilter($filter->getField(), [$condition => $filter->getValue()]); 82 | } 83 | } 84 | 85 | $sortOrders = $searchCriteria->getSortOrders(); 86 | if ($sortOrders) { 87 | foreach ($sortOrders as $sortOrder) { 88 | $collection->addOrder( 89 | $sortOrder->getField(), 90 | ($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC' 91 | ); 92 | } 93 | } 94 | $collection->setCurPage($searchCriteria->getCurrentPage()); 95 | $collection->setPageSize($searchCriteria->getPageSize()); 96 | 97 | return $collection->getItems(); 98 | } 99 | 100 | /** 101 | * Delete state 102 | * 103 | * @param \PandaGroup\StoreLocator\Api\Data\StatesInterface $state 104 | * @return bool 105 | * @throws \Magento\Framework\Exception\CouldNotDeleteException 106 | */ 107 | public function delete(\PandaGroup\StoreLocator\Api\Data\StatesInterface $state) 108 | { 109 | try { 110 | $this->resource->delete($state); 111 | } catch (\Exception $exception) { 112 | throw new CouldNotDeleteException(__($exception->getMessage())); 113 | } 114 | 115 | return true; 116 | } 117 | 118 | /** 119 | * Delete entity by ID. 120 | * 121 | * @param int $stateId 122 | * @return bool 123 | * @throws \Magento\Framework\Exception\NoSuchEntityException 124 | * @throws \Magento\Framework\Exception\CouldNotDeleteException 125 | */ 126 | public function deleteById($stateId) 127 | { 128 | return $this->delete($this->getById($stateId)); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /view/frontend/preact/src/container/DirectionsTab.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { connect } from 'mobx-preact'; 3 | 4 | @connect(['stateStore']) 5 | export default class DirectionsTab extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.handleChange = this.handleChange.bind(this); 9 | this.handleSubmit = this.handleSubmit.bind(this); 10 | this.handleGoogleAutocompletePick = this.handleGoogleAutocompletePick.bind(this); 11 | this.state = { 12 | start: props.stateStore.waypoints.start, 13 | stop: props.initStop, 14 | mode: props.stateStore.waypoints.mode, 15 | locked: 'b', 16 | }; 17 | this.textInputs = []; 18 | } 19 | 20 | handleChange(event) { 21 | this.setState({ 22 | [event.target.name]: event.target.value, 23 | }); 24 | } 25 | 26 | handleGoogleAutocompletePick(target) { 27 | this.setState({ 28 | [target.name]: target.value, 29 | }); 30 | } 31 | 32 | handleSubmit(event) { 33 | this.props.stateStore.updateWaypoints(this.state.start, this.state.stop, this.state.mode); 34 | event.preventDefault(); 35 | } 36 | 37 | swapAddress() { 38 | this.setState({ 39 | start: this.state.stop, 40 | stop: this.state.start, 41 | }); 42 | this.state.locked == 'a' ? this.setState({ locked: 'b' }) : this.setState({ locked: 'a' }); 43 | } 44 | 45 | transitCssClass(label) { 46 | const a = 'DirectionsTab__radio-label'; 47 | const active = this.state.mode === label ? `${a}--active ` : ''; 48 | const classes = `${a} ${a}--${label}`; 49 | return active + classes; 50 | } 51 | 52 | componentDidMount() { 53 | if (this.context.google) { 54 | this.textInputs.map(q => { 55 | this.initGoogleAutocomplete(q); 56 | }); 57 | } 58 | } 59 | 60 | componentWillUnmount() { 61 | this.directionsPanel.remove(); 62 | } 63 | 64 | initGoogleAutocomplete(target) { 65 | const options = { 66 | componentRestrictions: { country: this.context.constants.country }, 67 | }; 68 | 69 | const autocomplete = new this.context.google.maps.places.Autocomplete(target, options); 70 | 71 | autocomplete.addListener('place_changed', () => { 72 | this.handleGoogleAutocompletePick(target); 73 | }); 74 | } 75 | 76 | render() { 77 | return ( 78 |
    79 |
    80 | 90 | 99 | 108 |
    109 |
    110 |
    111 | 114 | { 123 | this.textInputs.push(input); 124 | }} 125 | /> 126 |
    127 |
    128 | 131 | { 140 | this.textInputs.push(input); 141 | }} 142 | /> 143 |
    144 |
    145 | 154 |
    155 | 160 |
    161 |
    { 164 | this.directionsPanel = div; 165 | }} 166 | /> 167 |
    168 | ); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /view/frontend/preact/src/util/ScriptCache.js: -------------------------------------------------------------------------------- 1 | let counter = 0; 2 | let scriptMap = window._scriptMap || new Map(); 3 | 4 | export const ScriptCache = (function(global) { 5 | global._scriptMap = global._scriptMap || scriptMap; 6 | return function ScriptCache(scripts) { 7 | const Cache = {}; 8 | 9 | Cache._onLoad = function(key) { 10 | return (cb) => { 11 | let stored = scriptMap.get(key); 12 | if (stored) { 13 | stored.promise.then(() => { 14 | stored.error ? cb(stored.error) : cb(null, stored); 15 | return stored; 16 | }); 17 | } else { 18 | // TODO: 19 | } 20 | }; 21 | }; 22 | 23 | Cache._scriptTag = (key, src) => { 24 | if (!scriptMap.has(key)) { 25 | let tag = document.createElement('script'); 26 | let promise = new Promise((resolve, reject) => { 27 | let resolved = false, 28 | errored = false, 29 | body = document.getElementsByTagName('body')[0]; 30 | 31 | tag.type = 'text/javascript'; 32 | tag.async = false; // Load in order 33 | 34 | const cbName = `loaderCB${counter++}${Date.now()}`; 35 | let cb; 36 | 37 | let handleResult = (state) => { 38 | return (evt) => { 39 | let stored = scriptMap.get(key); 40 | if (state === 'loaded') { 41 | stored.resolved = true; 42 | resolve(src); 43 | // stored.handlers.forEach(h => h.call(null, stored)) 44 | // stored.handlers = [] 45 | } else if (state === 'error') { 46 | stored.errored = true; 47 | // stored.handlers.forEach(h => h.call(null, stored)) 48 | // stored.handlers = []; 49 | reject(evt); 50 | } 51 | stored.loaded = true; 52 | 53 | cleanup(); 54 | }; 55 | }; 56 | 57 | const cleanup = () => { 58 | if (global[cbName] && typeof global[cbName] === 'function') { 59 | global[cbName] = null; 60 | delete global[cbName]; 61 | } 62 | }; 63 | 64 | tag.onload = handleResult('loaded'); 65 | tag.onerror = handleResult('error'); 66 | tag.onreadystatechange = () => { 67 | handleResult(tag.readyState); 68 | }; 69 | 70 | // Pick off callback, if there is one 71 | if (src.match(/callback=CALLBACK_NAME/)) { 72 | src = src.replace(/(callback=)[^\&]+/, `$1${cbName}`); 73 | cb = window[cbName] = tag.onload; 74 | } else { 75 | tag.addEventListener('load', tag.onload); 76 | } 77 | tag.addEventListener('error', tag.onerror); 78 | 79 | tag.src = src; 80 | body.appendChild(tag); 81 | 82 | return tag; 83 | }); 84 | let initialState = { 85 | loaded: false, 86 | error: false, 87 | promise: promise, 88 | tag 89 | }; 90 | scriptMap.set(key, initialState); 91 | } 92 | return scriptMap.get(key); 93 | }; 94 | 95 | // let scriptTags = document.querySelectorAll('script') 96 | // 97 | // NodeList.prototype.filter = Array.prototype.filter; 98 | // NodeList.prototype.map = Array.prototype.map; 99 | // const initialScripts = scriptTags 100 | // .filter(s => !!s.src) 101 | // .map(s => s.src.split('?')[0]) 102 | // .reduce((memo, script) => { 103 | // memo[script] = script; 104 | // return memo; 105 | // }, {}); 106 | 107 | Object.keys(scripts).forEach(function(key) { 108 | const script = scripts[key]; 109 | 110 | const tag = window._scriptMap.has(key) ? 111 | window._scriptMap.get(key).tag : 112 | Cache._scriptTag(key, script); 113 | 114 | Cache[key] = { 115 | tag: tag, 116 | onLoad: Cache._onLoad(key), 117 | }; 118 | }); 119 | 120 | return Cache; 121 | }; 122 | })(window); 123 | 124 | export default ScriptCache; 125 | -------------------------------------------------------------------------------- /Model/StoreLocatorRepository.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 36 | $this->storeLocatorFactory = $storeLocatorFactory; 37 | $this->collectionFactory = $collectionFactory; 38 | } 39 | 40 | /** 41 | * Save Store 42 | * 43 | * @param \PandaGroup\StoreLocator\Api\Data\StoreLocatorInterface $store 44 | * @return \PandaGroup\StoreLocator\Api\Data\StoreLocatorInterface 45 | * @throws \Magento\Framework\Exception\CouldNotSaveException 46 | */ 47 | public function save(\PandaGroup\StoreLocator\Api\Data\StoreLocatorInterface $store) 48 | { 49 | try { 50 | $this->resource->save($store); 51 | } catch (\Exception $exception) { 52 | throw new \Magento\Framework\Exception\CouldNotSaveException(__($exception->getMessage())); 53 | } 54 | 55 | return $store; 56 | } 57 | 58 | /** 59 | * Retrieve Store 60 | * 61 | * @param $storeId 62 | * @return \PandaGroup\StoreLocator\Model\StoreLocator 63 | * @throws \Magento\Framework\Exception\NoSuchEntityException 64 | */ 65 | public function getById($storeId) 66 | { 67 | $store = $this->storeLocatorFactory->create(); 68 | $this->resource->load($store, $storeId); 69 | if (!$store->getId()) { 70 | throw new NoSuchEntityException(__('Store with id "%1" does not exist.', $storeId)); 71 | } 72 | 73 | return $store; 74 | } 75 | 76 | /** 77 | * Retrieve entity matching the specified criteria. 78 | * 79 | * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria 80 | * @return \PandaGroup\StoreLocator\Api\Data\StatesInterface[] 81 | */ 82 | public function getList(SearchCriteriaInterface $searchCriteria) 83 | { 84 | $collection = $this->collectionFactory->create(); 85 | foreach ($searchCriteria->getFilterGroups() as $filterGroup) { 86 | foreach ($filterGroup->getFilters() as $filter) { 87 | $condition = $filter->getConditionType() ?: 'eq'; 88 | $collection->addFieldToFilter($filter->getField(), [$condition => $filter->getValue()]); 89 | } 90 | } 91 | 92 | $sortOrders = $searchCriteria->getSortOrders(); 93 | if ($sortOrders) { 94 | foreach ($sortOrders as $sortOrder) { 95 | $collection->addOrder( 96 | $sortOrder->getField(), 97 | ($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC' 98 | ); 99 | } 100 | } 101 | $collection->setCurPage($searchCriteria->getCurrentPage()); 102 | $collection->setPageSize($searchCriteria->getPageSize()); 103 | 104 | return $collection->getItems(); 105 | } 106 | 107 | /** 108 | * Delete store 109 | * 110 | * @param \PandaGroup\StoreLocator\Api\Data\StoreLocatorInterface $store 111 | * @return bool 112 | * @throws \Magento\Framework\Exception\CouldNotDeleteException 113 | */ 114 | public function delete(\PandaGroup\StoreLocator\Api\Data\StoreLocatorInterface $store) 115 | { 116 | try { 117 | $this->resource->delete($store); 118 | } catch (\Exception $exception) { 119 | throw new CouldNotDeleteException(__($exception->getMessage())); 120 | } 121 | 122 | return true; 123 | } 124 | 125 | /** 126 | * Delete entity by ID. 127 | * 128 | * @param int $storeId 129 | * @return bool 130 | * @throws \Magento\Framework\Exception\NoSuchEntityException 131 | * @throws \Magento\Framework\Exception\CouldNotDeleteException 132 | */ 133 | public function deleteById($storeId) 134 | { 135 | return $this->delete($this->getById($storeId)); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /view/frontend/preact/src/container/StoreView.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { connect } from 'mobx-preact'; 3 | 4 | import DirectionsTab from './../container/DirectionsTab'; 5 | import HoursSpanFill from './../component/HoursSpanFill'; 6 | 7 | @connect(['stateStore']) 8 | export default class StoreView extends Component { 9 | constructor(props, { router }) { 10 | super(props); 11 | this.state = { 12 | tab: 'directions', 13 | store: props.stores.find(this.findStore, this.routeId), 14 | }; 15 | this.routeId = router.route.match.params.name; 16 | this.state.store = props.stores.find(this.findStore, this.routeId); 17 | } 18 | 19 | componentDidUpdate() { 20 | const currentRoute = this.context.router.route.match.params.name; 21 | if (this.routeId !== currentRoute) { 22 | this.routeId = currentRoute; 23 | this.setState({ 24 | store: this.props.stores.find(this.findStore, this.routeId), 25 | }); 26 | this.props.stateStore.changeMap(this.state.store.geo, this.state.store.zoom); 27 | 28 | document.title = 29 | "Peter Jackson's " + this.state.store.name + ' ' + this.state.store.addr_cty; 30 | document.getElementsByClassName('storelocator-header__title')[0].innerHTML = 31 | "Peter Jackson's " + this.state.store.name + ' ' + this.state.store.addr_cty; 32 | } 33 | } 34 | 35 | componentDidMount() { 36 | document.title = 37 | "Peter Jackson's " + this.state.store.name + ' ' + this.state.store.addr_cty; 38 | document.getElementsByClassName('storelocator-header__title')[0].innerHTML = 39 | "Peter Jackson's " + this.state.store.name + ' ' + this.state.store.addr_cty; 40 | this.props.stateStore.changeMap(this.state.store.geo, this.state.store.zoom); 41 | } 42 | 43 | findStore(q) { 44 | return ( 45 | q.name 46 | .split(' ') 47 | .join('-') 48 | .toLowerCase() == this 49 | ); 50 | } 51 | 52 | showTab(active, set) { 53 | return active == set ? 'display: block' : 'display: none'; 54 | } 55 | 56 | setTab(selected) { 57 | this.setState({ tab: selected }); 58 | } 59 | 60 | setTabClass(selected) { 61 | return this.state.tab == selected ? 'tabs__tab tabs__tab--active' : 'tabs__tab'; 62 | } 63 | 64 | render() { 65 | const tab1 = 'hours'; 66 | const tab2 = 'directions'; 67 | return ( 68 |
    69 |
    70 |

    {this.state.store.name}

    71 | 88 |
    89 |
    90 |
    91 |
    this.setTab(tab1)}> 92 | Store Information 93 |
    94 |
    this.setTab(tab2)}> 95 | Directions 96 |
    97 |
    98 |
    99 |
    100 |

    Opening Hours

    101 | 102 |
    103 |
    107 | 112 |
    113 |
    114 |
    115 |
    116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Model/Config/Source/ListState.php: -------------------------------------------------------------------------------- 1 | countriesData = $countriesData; 33 | $this->regionsData = $regionsData; 34 | $this->_catalogConfig = $catalogConfig; 35 | } 36 | 37 | /** 38 | * Retrieve option values array 39 | * 40 | * @return array 41 | */ 42 | public function toOptionArray() 43 | { 44 | $options = []; 45 | $options[] = ['label' => __(' '), 'value' => ' ']; 46 | foreach ($this->getRegionsAsArray() as $regionId => $regionName) { 47 | $options[] = ['label' => __($regionName), 'value' => $regionId]; 48 | } 49 | return $options; 50 | } 51 | 52 | /** 53 | * Retrieve Catalog Config Singleton 54 | * 55 | * @return \Magento\Catalog\Model\Config 56 | */ 57 | protected function _getCatalogConfig() 58 | { 59 | return $this->_catalogConfig; 60 | } 61 | 62 | public function getRegionsAsArray($countryCode = '') 63 | { 64 | /** @var \PandaGroup\StoreLocator\Model\ResourceModel\RegionsData\Collection $regionsDataCollection */ 65 | $regionsDataCollection = $this->regionsData->getCollection(); 66 | 67 | /** @var \PandaGroup\StoreLocator\Model\ResourceModel\CountriesData\Collection $countriesDataCollection */ 68 | $countriesDataCollection = $this->countriesData->getCollection(); 69 | 70 | if (false === empty($countryCode)) { 71 | $countriesDataCollection->addFilter('code', $countryCode); 72 | $countryId = $countriesDataCollection->getFirstItem()->getId(); 73 | if (false === isset($countryId)) return null; 74 | $regionsDataCollection->addFilter('country_id', $countryId); 75 | } 76 | 77 | $regionsByCountry = []; 78 | $emptyRegionId = null; 79 | /* 80 | foreach ($regionsDataCollection as $region) { 81 | // if (false === empty($region->getName())) { // Some rows in the database are empty 82 | // $regionsByCountry[$region->getId()] = $region->getName(); 83 | // } 84 | 85 | if (false === empty($region->getName())) { // Some rows in the database are empty 86 | $regionsByCountry[$region->getId()] = $region->getName(); 87 | } else { 88 | // $regionsByCountry[$region->getId()] = '-'; 89 | $emptyRegionId = $region->getId(); 90 | } 91 | } 92 | 93 | if (true === empty($regionsByCountry)) { 94 | $regionsByCountry[$emptyRegionId] = $countriesDataCollection->addFilter('code', $countryCode)->getFirstItem()->getData('name'); 95 | $regionsByCountry[''] = ' '; 96 | } 97 | */ 98 | 99 | foreach ($regionsDataCollection as $region) { 100 | 101 | if (false === empty($region->getName())) { // Some rows in the database are empty 102 | $regionsByCountry[$region->getId()] = $region->getName(); 103 | } else { 104 | $regionsByCountry[$region->getId()] = '-'; 105 | $emptyRegionId = $region->getId(); 106 | } 107 | } 108 | 109 | $isArrayOnlyOfEmptyRegions = true; 110 | foreach ($regionsByCountry as $simpleRegion) { 111 | if ($simpleRegion != '-') { 112 | $isArrayOnlyOfEmptyRegions = false; 113 | break; 114 | } 115 | } 116 | 117 | // Clear empty regions only if there are some correct regions (nax to empty regions) 118 | if ($countryCode != '') { 119 | foreach ($regionsByCountry as $key => $value) { 120 | if ($value == '-') { 121 | unset($regionsByCountry[$key]); 122 | } 123 | } 124 | } 125 | 126 | // If country has any region show country name on region list witch id form database 127 | // Countries without regions have one empty region 128 | if (true === empty($regionsByCountry) || $isArrayOnlyOfEmptyRegions) { 129 | $regionsByCountry[$emptyRegionId] = $countriesDataCollection->addFilter('code', $countryCode)->getFirstItem()->getData('name'); 130 | $regionsByCountry[''] = ' '; 131 | } 132 | 133 | return $regionsByCountry; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Model/GoogleApi.php: -------------------------------------------------------------------------------- 1 | configProvider = $configProvider; 41 | $this->jsonHelper = $jsonHelper; 42 | $this->logger = $logger; 43 | } 44 | 45 | /** 46 | * @param $addressName 47 | * @return mixed 48 | */ 49 | public function getCoordinatesByAddress($addressName) 50 | { 51 | $this->logger->info('Start getting coordinates from Google Api.'); 52 | $apiKey = $this->configProvider->getGoogleApiKey(); 53 | 54 | $addressName = urlencode($addressName); 55 | 56 | $url = self::GOOGLE_API_ADDRESS_URL . $addressName . '&key=' . $apiKey; 57 | 58 | $ch = curl_init(); 59 | 60 | curl_setopt($ch, CURLOPT_URL, $url); 61 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 62 | $output = curl_exec($ch); 63 | curl_close($ch); 64 | 65 | $countryInformation = $this->jsonHelper->jsonDecode($output); 66 | 67 | if (isset($countryInformation['status'])) { 68 | $requestStatus = $countryInformation['status']; 69 | } 70 | 71 | if (isset($countryInformation['error_message'])) { 72 | $requesterrorMessage = $countryInformation['error_message']; 73 | } else $requesterrorMessage = 'Undefined Google Api error'; 74 | 75 | if ($requestStatus == 'OK') { 76 | $this->logger->info(' Success getting response from Google Api.'); 77 | } else { 78 | $this->logger->error(' Error while getting response from Google Api: '.$requesterrorMessage); 79 | } 80 | 81 | if (isset($countryInformation['results'][0]['geometry']['location']['lat']) 82 | && isset($countryInformation['results'][0]['geometry']['location']['lng']) 83 | ) { 84 | $coordinates['lat'] = $countryInformation['results'][0]['geometry']['location']['lat']; 85 | $coordinates['lng'] = $countryInformation['results'][0]['geometry']['location']['lng']; 86 | $this->logger->info(' Success getting coordinates from Google Api response.'); 87 | } else { 88 | $coordinates = null; 89 | $this->logger->error(' Error getting coordinates from Google Api response.'); 90 | } 91 | 92 | $this->logger->info('Finish getting coordinates from Google Api.'); 93 | return $coordinates; 94 | } 95 | 96 | /** 97 | * @param $regionName 98 | * @return null 99 | */ 100 | public function getRegionShortName($regionName) 101 | { 102 | $apiKey = $this->configProvider->getGoogleApiKey(); 103 | 104 | $regionName = urlencode($regionName); 105 | 106 | $url = self::GOOGLE_API_ADDRESS_URL . $regionName . '&key=' . $apiKey; 107 | 108 | $ch = curl_init(); 109 | 110 | curl_setopt($ch, CURLOPT_URL, $url); 111 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 112 | $output = curl_exec($ch); 113 | curl_close($ch); 114 | 115 | $countryInformation = $this->jsonHelper->jsonDecode($output); 116 | 117 | if (isset($countryInformation['status'])) { 118 | $requestStatus = $countryInformation['status']; 119 | } 120 | 121 | if (isset($countryInformation['error_message'])) { 122 | $requesterrorMessage = $countryInformation['status']; 123 | } 124 | 125 | $shortStateName = null; 126 | foreach ($countryInformation['results'] as $region) { 127 | 128 | if (false === isset($region['address_components'])) continue; 129 | 130 | foreach ($region['address_components'] as $addressComponent) { 131 | 132 | if (false === isset($addressComponent['types'][0])) continue; 133 | 134 | if ($addressComponent['types'][0] === 'administrative_area_level_1') { 135 | $shortStateName = $addressComponent['short_name']; 136 | } 137 | } 138 | } 139 | 140 | return $shortStateName; 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /view/frontend/preact/src/container/StoreHeader.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { connect } from 'mobx-preact'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | import GoogleApiComponent from './../component/GoogleApiComponent'; 6 | import HeaderMap from './HeaderMap'; 7 | import RegionFilter from './../component/RegionFilter'; 8 | 9 | @connect(['stateStore']) 10 | export default class StoreHeader extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.postcodeInput = null; 14 | this.geocode = null; 15 | this.applyFilter = this.applyFilter.bind(this); 16 | this.resetFilters = this.resetFilters.bind(this); 17 | this.searchPostcode = this.searchPostcode.bind(this); 18 | } 19 | 20 | componentWillMount() { 21 | this.props.stateStore.initializeStore(this.context.router); 22 | } 23 | 24 | resetFilters() { 25 | this.props.stateStore.clearFilters(); 26 | } 27 | 28 | searchPostcode() { 29 | if (!this.geocode) { 30 | this.geocode = new this.props.google.maps.Geocoder(); 31 | } 32 | this.geocode.geocode( 33 | { 34 | componentRestrictions: { 35 | country: this.context.constants.country, 36 | postalCode: this.postcodeInput.value, 37 | }, 38 | }, 39 | (results, status) => { 40 | if (status === 'OK') { 41 | const newGeo = { 42 | lat: results[0].geometry.location.lat(), 43 | lng: results[0].geometry.location.lng(), 44 | }; 45 | this.props.stateStore.setError(); 46 | this.props.stateStore.changeMap(newGeo, '11'); 47 | } else { 48 | this.props.stateStore.setError('Invalid postcode'); 49 | } 50 | } 51 | ); 52 | } 53 | 54 | isActiveFilter(region) { 55 | return this.props.stateStore.filters.indexOf(region) > -1 56 | ? 'storelocator-header__filter storelocator-header__filter--active' 57 | : 'storelocator-header__filter'; 58 | } 59 | 60 | backButton() { 61 | this.props.stateStore.changeView(); 62 | this.props.stateStore.changeMap(); 63 | } 64 | 65 | applyFilter(region) { 66 | this.props.stateStore.addFilters(region); 67 | } 68 | 69 | render() { 70 | const { regions } = this.props; 71 | return ( 72 |
    73 |

    Store Locator

    74 |
    75 | 88 |
    89 |
    90 | {this.props.stateStore.view === 'list' ? ( 91 |
    92 | {regions.map(region => ( 93 | 98 | ))} 99 |
    100 | ) : ( 101 | this.backButton()} 104 | className="storelocator-header__back-a" 105 | > 106 | 107 | 108 | )} 109 |
    110 | 111 | Reset 112 | 113 | { 116 | this.postcodeInput = input; 117 | }} 118 | placeholder="Postcode" 119 | type="text" 120 | name="postcode" 121 | /> 122 | 128 |
    129 |
    130 |

    {this.props.stateStore.error}

    131 | 132 |
    133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Ui/Component/Listing/Column/IndexActions.php: -------------------------------------------------------------------------------- 1 | urlBuilder = $urlBuilder; 40 | parent::__construct($context, $uiComponentFactory, $components, $data); 41 | } 42 | 43 | /** 44 | * Prepare Data Source 45 | * 46 | * @param array $dataSource 47 | * 48 | * @return array 49 | */ 50 | public function prepareDataSource(array $dataSource) 51 | { 52 | if (isset($dataSource['data']['items'])) { 53 | foreach ($dataSource['data']['items'] as & $item) { 54 | if (isset($item['storelocator_id'])) { 55 | $item[$this->getData('name')] = [ 56 | 'edit' => [ 57 | 'href' => $this->urlBuilder->getUrl( 58 | static::URL_PATH_EDIT, 59 | [ 60 | 'id' => $item['storelocator_id'] 61 | ] 62 | ), 63 | 'label' => __('Edit') 64 | ], 65 | 'delete' => [ 66 | 'href' => $this->urlBuilder->getUrl( 67 | static::URL_PATH_DELETE, 68 | [ 69 | 'id' => $item['storelocator_id'] 70 | ] 71 | ), 72 | 'label' => __('Delete'), 73 | 'confirm' => [ 74 | 'title' => __('Delete "${ $.$data.name }"'), 75 | 'message' => __('Are you sure you wan\'t to delete a "${ $.$data.name }" store?') 76 | ] 77 | ] 78 | ]; 79 | } 80 | } 81 | } 82 | 83 | return $dataSource; 84 | } 85 | 86 | 87 | // /** 88 | // * Prepare Data Source 89 | // * 90 | // * @param array $dataSource 91 | // * @return array 92 | // */ 93 | // public function prepareDataSource(array $dataSource) 94 | // { 95 | ///* 96 | // if (isset($dataSource['data']['items'])) { 97 | // foreach ($dataSource['data']['items'] as & $item) { 98 | // if (isset($item['post_id'])) { 99 | // $item[$this->getData('name')] = [ 100 | // 'edit' => [ 101 | // 'href' => $this->_urlBuilder->getUrl( 102 | // static::URL_PATH_EDIT, 103 | // [ 104 | // 'post_id' => $item['post_id'] 105 | // ] 106 | // ), 107 | // 'label' => __('Edit') 108 | // ], 109 | // 'delete' => [ 110 | // 'href' => $this->_urlBuilder->getUrl( 111 | // static::URL_PATH_DELETE, 112 | // [ 113 | // 'post_id' => $item['post_id'] 114 | // ] 115 | // ), 116 | // 'label' => __('Delete'), 117 | // 'confirm' => [ 118 | // 'title' => __('Delete "${ $.$data.name }"'), 119 | // 'message' => __('Are you sure you wan\'t to delete the Post "${ $.$data.name }" ?') 120 | // ] 121 | // ] 122 | // ]; 123 | // } 124 | // } 125 | // } 126 | //*/ 127 | // if (isset($dataSource['data']['items'])) { 128 | // foreach ($dataSource['data']['items'] as &$item) { 129 | // 130 | // $name = $this->getData('name'); 131 | // if (isset($item['id']) && $this->isOrderIncrementId($item['id'])) { 132 | // $item[$name]['view_order'] = [ 133 | // 134 | // 135 | // 'href' => $this->getRowUrl($item), 136 | // 137 | // 138 | // 'label' => __('View Store'), 139 | // ]; 140 | // } 141 | // } 142 | // } 143 | // 144 | // return $dataSource; 145 | // } 146 | // 147 | // public function isOrderIncrementId($orderId) 148 | // { 149 | // $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); 150 | // $orderInterface = $objectManager->create('\Magento\Sales\Api\Data\OrderInterface'); 151 | // $order = $orderInterface->load($orderId); 152 | // if($order->getId()) { 153 | // return true; 154 | // } 155 | // return false; 156 | // } 157 | // 158 | // public function getRowUrl($item) 159 | // { 160 | // return $this->_urlBuilder->getUrl('storelocator/index/index', ['id' => $item['id']]); 161 | // } 162 | } 163 | -------------------------------------------------------------------------------- /view/frontend/preact/src/component/Map.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { h, Component } from 'preact'; 3 | import camelize from './../util/Camelize'; 4 | 5 | const mapStyles = { 6 | container: { 7 | position: 'absolute', 8 | width: '100%', 9 | height: '100%', 10 | }, 11 | map: { 12 | position: 'absolute', 13 | left: 0, 14 | right: 0, 15 | bottom: 0, 16 | top: 0, 17 | }, 18 | }; 19 | 20 | const evtNames = [ 21 | 'ready', 22 | 'click', 23 | 'dragend', 24 | 'recenter', 25 | 'bounds_changed', 26 | 'center_changed', 27 | 'dblclick', 28 | 'dragstart', 29 | 'heading_change', 30 | 'idle', 31 | 'maptypeid_changed', 32 | 'mousemove', 33 | 'mouseout', 34 | 'mouseover', 35 | 'projection_changed', 36 | 'resize', 37 | 'rightclick', 38 | 'tilesloaded', 39 | 'tilt_changed', 40 | 'zoom_changed', 41 | ]; 42 | 43 | export default class Map extends Component { 44 | constructor(props) { 45 | super(props); 46 | this.listeners = {}; 47 | this.state = { 48 | currentLocation: { 49 | lat: this.props.initialCenter.lat, 50 | lng: this.props.initialCenter.lng, 51 | }, 52 | }; 53 | } 54 | 55 | componentDidMount() { 56 | this.loadMap(); 57 | } 58 | 59 | componentDidUpdate(prevProps, prevState) { 60 | if (prevProps.google !== this.props.google) { 61 | this.loadMap(); 62 | } 63 | if (this.props.visible !== prevProps.visible) { 64 | this.restyleMap(); 65 | } 66 | if (this.props.zoom !== prevProps.zoom) { 67 | this.map.setZoom(Number.parseInt(this.props.zoom)); 68 | } 69 | if (this.props.center !== prevProps.center) { 70 | this.setState({ 71 | currentLocation: this.props.center, 72 | }); 73 | } 74 | if (prevState.currentLocation !== this.state.currentLocation) { 75 | this.recenterMap(); 76 | } 77 | } 78 | 79 | componentWillUnmount() { 80 | const { google } = this.props; 81 | if (this.geoPromise) { 82 | this.geoPromise.cancel(); 83 | } 84 | Object.keys(this.listeners).forEach(e => { 85 | google.maps.event.removeListener(this.listeners[e]); 86 | }); 87 | } 88 | 89 | loadMap() { 90 | if (this.props && this.props.google) { 91 | const { google } = this.props; 92 | const maps = google.maps; 93 | 94 | const node = this.mapDiv; 95 | const curr = this.state.currentLocation; 96 | const center = new maps.LatLng(curr.lat, curr.lng); 97 | 98 | const mapTypeIds = this.props.google.maps.MapTypeId || {}; 99 | const mapTypeFromProps = String(this.props.mapType).toUpperCase(); 100 | 101 | const mapConfig = Object.assign( 102 | {}, 103 | { 104 | mapTypeId: mapTypeIds[mapTypeFromProps], 105 | center: center, 106 | zoom: Number.parseInt(this.props.zoom), 107 | maxZoom: Number.parseInt(this.props.maxZoom), 108 | minZoom: Number.parseInt(this.props.maxZoom), 109 | clickableIcons: this.props.clickableIcons, 110 | disableDefaultUI: this.props.disableDefaultUI, 111 | zoomControl: this.props.zoomControl, 112 | mapTypeControl: this.props.mapTypeControl, 113 | scaleControl: this.props.scaleControl, 114 | streetViewControl: this.props.streetViewControl, 115 | panControl: this.props.panControl, 116 | rotateControl: this.props.rotateControl, 117 | scrollwheel: this.props.scrollwheel, 118 | draggable: this.props.draggable, 119 | keyboardShortcuts: this.props.keyboardShortcuts, 120 | disableDoubleClickZoom: this.props.disableDoubleClickZoom, 121 | noClear: this.props.noClear, 122 | styles: this.props.styles, 123 | gestureHandling: this.props.gestureHandling, 124 | } 125 | ); 126 | 127 | Object.keys(mapConfig).forEach(key => { 128 | if (mapConfig[key] === null) { 129 | delete mapConfig[key]; 130 | } 131 | }); 132 | 133 | this.map = new maps.Map(node, mapConfig); 134 | 135 | evtNames.forEach(e => { 136 | this.listeners[e] = this.map.addListener(e, this.handleEvent(e)); 137 | }); 138 | maps.event.trigger(this.map, 'ready'); 139 | this.forceUpdate(); 140 | } 141 | } 142 | 143 | handleEvent(evtName) { 144 | let timeout; 145 | const handlerName = `on${camelize(evtName)}`; 146 | 147 | return e => { 148 | if (timeout) { 149 | clearTimeout(timeout); 150 | timeout = null; 151 | } 152 | timeout = setTimeout(() => { 153 | if (this.props[handlerName]) { 154 | this.props[handlerName](this.props, this.map, e); 155 | } 156 | }, 0); 157 | }; 158 | } 159 | 160 | recenterMap() { 161 | if (this.props && this.props.google) { 162 | const map = this.map; 163 | 164 | const { google } = this.props; 165 | const maps = google.maps; 166 | 167 | if (!google) return; 168 | 169 | if (map) { 170 | let center = this.state.currentLocation; 171 | if (!(center instanceof google.maps.LatLng)) { 172 | center = new google.maps.LatLng(center.lat, center.lng); 173 | } 174 | map.setCenter(center); 175 | maps.event.trigger(map, 'recenter'); 176 | } 177 | } 178 | } 179 | 180 | restyleMap() { 181 | if (this.map) { 182 | const { google } = this.props; 183 | google.maps.event.trigger(this.map, 'resize'); 184 | } 185 | } 186 | 187 | renderChildren() { 188 | const { children } = this.props; 189 | 190 | if (!children) return; 191 | 192 | return React.Children.map(children, c => { 193 | return React.cloneElement(c, { 194 | map: this.map, 195 | google: this.props.google, 196 | mapCenter: this.state.currentLocation, 197 | }); 198 | }); 199 | } 200 | 201 | render() { 202 | const style = Object.assign({}, mapStyles.map, this.props.style, { 203 | display: this.props.visible ? 'inherit' : 'none', 204 | }); 205 | 206 | const containerStyles = Object.assign({}, mapStyles.container, this.props.containerStyle); 207 | 208 | return ( 209 |
    210 |
    { 212 | this.mapDiv = div; 213 | }} 214 | style={style} 215 | > 216 |
    217 | {this.renderChildren()} 218 |
    219 | ); 220 | } 221 | } 222 | 223 | Map.defaultProps = { 224 | zoom: 14, 225 | initialCenter: { 226 | lat: 37.774929, 227 | lng: -122.419416, 228 | }, 229 | center: {}, 230 | centerAroundCurrentLocation: false, 231 | style: {}, 232 | containerStyle: {}, 233 | visible: true, 234 | }; 235 | -------------------------------------------------------------------------------- /Helper/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | scopeConfig = $context->getScopeConfig(); 55 | $this->storeManager = $storeManager; 56 | parent::__construct($context); 57 | } 58 | 59 | /** 60 | * Retrieve Google API Key 61 | * 62 | * @param null $store 63 | * @return string 64 | */ 65 | public function getGoogleApiKey($store = null) 66 | { 67 | return (string) $this->scopeConfig->getValue( 68 | self::STORE_LOCATOR_SECTION . self::STORE_LOCATOR_BASE_SETTINGS_GROUP . self::GOOGLE_API_KEY_FIELD, 69 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 70 | $store 71 | ); 72 | } 73 | 74 | /** 75 | * Retrieve Country of stores location 76 | * 77 | * @param null $store 78 | * @return string 79 | */ 80 | public function getStoresLocationCountryCode($store = null) 81 | { 82 | return (string) $this->scopeConfig->getValue( 83 | self::STORE_LOCATOR_SECTION . self::STORE_LOCATOR_BASE_SETTINGS_GROUP . self::COUNTRY_FIELD, 84 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 85 | $store 86 | ); 87 | } 88 | 89 | /** 90 | * Retrieve Hours Time Format 91 | * 92 | * @param null $store 93 | * @return string 94 | */ 95 | public function getHoursTimeFormat($store = null) 96 | { 97 | return (int) $this->scopeConfig->getValue( 98 | self::STORE_LOCATOR_SECTION . self::STORE_LOCATOR_BASE_SETTINGS_GROUP . self::TIME_FORMAT_FIELD, 99 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 100 | $store 101 | ); 102 | } 103 | 104 | /** 105 | * Retrieve Latitude of map 106 | * 107 | * @param null $store 108 | * @return string 109 | */ 110 | public function getMapLatitude($store = null) 111 | { 112 | return (string) $this->scopeConfig->getValue( 113 | self::STORE_LOCATOR_SECTION . self::STORE_LOCATOR_MAP_SETTINGS_GROUP . self::LATITUDE_FIELD, 114 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 115 | $store 116 | ); 117 | } 118 | 119 | /** 120 | * Retrieve Longitude of map 121 | * 122 | * @param null $store 123 | * @return string 124 | */ 125 | public function getMapLongitude($store = null) 126 | { 127 | return (string) $this->scopeConfig->getValue( 128 | self::STORE_LOCATOR_SECTION . self::STORE_LOCATOR_MAP_SETTINGS_GROUP . self::LONGITUDE_FIELD, 129 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 130 | $store 131 | ); 132 | } 133 | 134 | /** 135 | * Retrieve Zoom Level of map 136 | * 137 | * @param null $store 138 | * @return string 139 | */ 140 | public function getMapZoomLevel($store = null) 141 | { 142 | return (int) $this->scopeConfig->getValue( 143 | self::STORE_LOCATOR_SECTION . self::STORE_LOCATOR_MAP_SETTINGS_GROUP . self::ZOOM_LEVEL_FIELD, 144 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 145 | $store 146 | ); 147 | } 148 | 149 | /** 150 | * Retrieve Pin image link of map spinner 151 | * 152 | * @param null $store 153 | * @return string 154 | */ 155 | public function getPinImageLink($store = null) 156 | { 157 | $filePath = (string) $this->scopeConfig->getValue( 158 | self::STORE_LOCATOR_SECTION . self::STORE_LOCATOR_MAP_SETTINGS_GROUP . self::PIN_IMAGE_LINK_FIELD, 159 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 160 | $store 161 | ); 162 | 163 | $dir = \PandaGroup\StoreLocator\Model\Config\Backend\Image::UPLOAD_DIR . DIRECTORY_SEPARATOR; 164 | return $this->getMediaUrl() . $dir . $filePath; 165 | } 166 | 167 | /** 168 | * Retrieve Debug Status 169 | * 170 | * @param null $store 171 | * @return bool 172 | */ 173 | public function getDebugStatus($store = null) 174 | { 175 | return (bool) $this->scopeConfig->getValue( 176 | self::STORE_LOCATOR_SECTION . self::STORE_LOCATOR_ADVANCED_GROUP . self::DEBUG_STATUS_FIELD, 177 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 178 | $store 179 | ); 180 | } 181 | 182 | /** 183 | * Retrieve MEDIA path 184 | * 185 | * @return string 186 | */ 187 | public function getMediaUrl() 188 | { 189 | return $this->_urlBuilder->getBaseUrl(['_type' => \Magento\Framework\UrlInterface::URL_TYPE_MEDIA]); 190 | } 191 | } 192 | --------------------------------------------------------------------------------