diff --git a/.gitignore b/.gitignore index 391f47d..bbef309 100644 --- a/.gitignore +++ b/.gitignore @@ -114,6 +114,7 @@ build/ dist/ out/ site/ +!source/packages/*/site/ *.map *.css.map *.js.map diff --git a/CHANGELOG.md b/CHANGELOG.md index b95424b..d68fa61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,45 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - -### Removed -- Removed deploy-manual.yml workflow — switching to Joomla update server method for extension distribution +## [1.0.0] - 2026-06-23 ### Added +- Admin `LocationController` (FormController) for single-record save/cancel/apply +- Admin `LocationsController` (AdminController) for bulk publish/unpublish/delete +- Admin location edit view and tabbed template (Details, Address, Contact) +- Admin locations list renders data rows with edit links and published toggle +- `LocationTable::check()` validation: required title, auto-alias, lat/lng range, timestamps +- `LocationsModel::populateState()` for filter persistence +- Search filter across title, city, state, address +- Published state filter and sort ordering support +- Filter form XML (`filter_locations.xml`) with search tools bar +- Language strings for filters, sort options, and save messages +- Site frontend `DisplayController` routing to list and detail views +- Site `LocationsModel` — published locations with search, city, and state filters +- Site `LocationModel` — single location by ID (published only) +- Site locations list view with Schema.org `LocalBusiness` markup and pagination +- Site location detail view with address, contact, hours, and map placeholder +- SEF URL router (`Service\Router`) with menu/standard/nomenu rules +- Menu item types: "All Locations" list and "Location Detail" with location picker +- Site language strings for frontend views and menu items +- Router registered in service provider and component extension class +- Map module dispatcher loads published locations with coordinates from DB +- Leaflet.js/OpenStreetMap integration with markers, popups, and auto-fit bounds +- Leaflet CSS/JS loaded via Joomla Web Asset Manager (`registerAndUseStyle`/`registerAndUseScript`) +- Search module dispatcher loads distinct cities/states and builds radius options +- City dropdown filter on search form (populated from DB, toggled by module param) +- Radius dropdown filter with configurable distance values and unit (miles/km) +- Geolocation "Use My Location" button with browser geolocation API +- Hidden lat/lng fields passed to component for proximity search +- Language strings for search module (city, radius, geolocation states) + +### Removed +- Makefile (no longer used) +- deploy-manual.yml workflow + +### Previous (scaffold) - Initial package scaffold with component, map module, and search module - Database schema for locations table with coordinates -- Admin MVC for location CRUD -- Frontend location listing view with Schema.org markup -- Map module with Leaflet/Google Maps provider support -- Search module with city and radius filter options +- Admin MVC skeleton for location CRUD +- Map module with Leaflet/Google Maps provider support (stub) +- Search module with city and radius filter options (stub) diff --git a/CLAUDE.md b/CLAUDE.md index e65deda..eea75a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,37 +4,29 @@ This file provides guidance to Claude Code when working with this repository. ## Project Overview -**MokoJoomStoreLocator** -- A Joomla 4/5 package providing a store locator listing component with coordinating map and search modules. +**MokoSuiteStoreLocator** -- A Joomla 5/6 package providing a store locator listing component with coordinating map and search modules. | Field | Value | |---|---| | **Platform** | joomla | | **Extension type** | package (component + modules) | -| **Element** | `pkg_mokojoomstorelocator` | +| **Element** | `pkg_mokosuitestorelocator` | | **Language** | PHP | | **Default branch** | main | | **License** | GPL-3.0-or-later | -| **Wiki** | [MokoJoomStoreLocator Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomStoreLocator/wiki) | +| **Wiki** | [MokoSuiteStoreLocator Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteStoreLocator/wiki) | | **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) | ## Package Contents | Extension | Type | Element | |---|---|---| -| Store Locator Component | component | `com_mokojoomstorelocator` | -| Store Locator Map | module (site) | `mod_mokojoomstorelocator_map` | -| Store Locator Search | module (site) | `mod_mokojoomstorelocator_search` | +| Store Locator Component | component | `com_mokosuitestorelocator` | +| Store Locator Map | module (site) | `mod_mokosuitestorelocator_map` | +| Store Locator Search | module (site) | `mod_mokosuitestorelocator_search` | ## Common Commands -```bash -make build # Build package ZIP containing all sub-extensions -make lint # Run PHP linter -make validate # Lint + validation checks -make release # Validate + build -make clean # Clean build artifacts -``` - ```bash composer install # Install PHP dev dependencies ``` @@ -43,19 +35,18 @@ composer install # Install PHP dev dependencies This is a Joomla package. Key layout: -- `src/pkg_mokojoomstorelocator.xml` -- package manifest -- `src/script.php` -- package install/upgrade/uninstall script -- `src/packages/com_mokojoomstorelocator/` -- main component +- `source/pkg_mokosuitestorelocator.xml` -- package manifest +- `source/script.php` -- package install/upgrade/uninstall script +- `source/packages/com_mokosuitestorelocator/` -- main component - `admin/` -- admin MVC (controllers, models, views, forms, tables, SQL) - `site/` -- frontend MVC (controllers, models, views, templates) - - `mokojoomstorelocator.xml` -- component manifest -- `src/packages/mod_mokojoomstorelocator_map/` -- map display module -- `src/packages/mod_mokojoomstorelocator_search/` -- search/filter module -- `updates.xml` -- Joomla update server manifest + - `mokosuitestorelocator.xml` -- component manifest +- `source/packages/mod_mokosuitestorelocator_map/` -- map display module +- `source/packages/mod_mokosuitestorelocator_search/` -- search/filter module ## Database Table -`#__mokojoomstorelocator_locations` -- stores location data including coordinates, address, contact info, and business hours. +`#__mokosuitestorelocator_locations` -- stores location data including coordinates, address, contact info, and business hours. ## Rules @@ -66,6 +57,7 @@ This is a Joomla package. Key layout: - **Branch strategy**: develop on `dev/`, merge to `main` for release - **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files - **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) -- **PHP minimum**: 8.1 +- **PHP minimum**: 8.2 +- **Joomla minimum**: 5.0 - **Joomla table operations**: always use bind() -> check() -> store(), never save() -- **Namespace**: `Moko\Component\MokoJoomStoreLocator` for the component +- **Namespace**: `Moko\Component\MokoSuiteStoreLocator` for the component diff --git a/Makefile b/Makefile deleted file mode 100644 index 91f9ee9..0000000 --- a/Makefile +++ /dev/null @@ -1,135 +0,0 @@ -# Makefile for Joomla Extensions -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This is a reference Makefile for building Joomla extensions. -# Copy this to your repository root as "Makefile" and customize as needed. -# -# Supports: Modules, Plugins, Components, Packages, Templates - -# ============================================================================== -# CONFIGURATION - Customize these for your extension -# ============================================================================== - -# Extension Configuration -EXTENSION_NAME := mokojoomstorelocator -EXTENSION_TYPE := package -# Options: module, plugin, component, package, template -EXTENSION_VERSION := 1.0.0 - -# Module Configuration (for modules only) -MODULE_TYPE := site -# Options: site, admin - -# Plugin Configuration (for plugins only) -PLUGIN_GROUP := system -# Options: system, content, user, authentication, etc. - -# Directories -SRC_DIR := . -BUILD_DIR := build -DIST_DIR := dist -DOCS_DIR := docs - -# Joomla Installation (for local testing - customize paths) -JOOMLA_ROOT := /var/www/html/joomla -JOOMLA_VERSION := 5 - -# Tools -PHP := php -COMPOSER := composer -NPM := npm -PHPCS := vendor/bin/phpcs -PHPCBF := vendor/bin/phpcbf -PHPUNIT := vendor/bin/phpunit -ZIP := zip - -# Coding Standards -PHPCS_STANDARD := Joomla - -# Colors for output -COLOR_RESET := \033[0m -COLOR_GREEN := \033[32m -COLOR_YELLOW := \033[33m -COLOR_BLUE := \033[34m -COLOR_RED := \033[31m - -# ============================================================================== -# TARGETS -# ============================================================================== - -.PHONY: help -help: ## Show this help message - @echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)" - @echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)" - @echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)" - @echo "" - @echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)" - @echo "" - @echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}' - @echo "" - -.PHONY: lint -lint: ## Run PHP linter (syntax check) - @echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)" - @find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \ - -exec $(PHP) -l {} \; | grep -v "No syntax errors" || true - @echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)" - -.PHONY: validate -validate: lint ## Run all validation checks - @echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)" - -.PHONY: clean -clean: ## Clean build artifacts - @echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)" - @rm -rf $(BUILD_DIR) $(DIST_DIR) - @echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)" - -MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform)) -MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js - -.PHONY: minify -minify: ## Minify CSS/JS assets - @echo "Minifying assets..." - @if [ -f "$(MINIFY_SCRIPT)" ]; then \ - node "$(MINIFY_SCRIPT)" $(SRC_DIR); \ - elif [ -f "scripts/minify.js" ]; then \ - node scripts/minify.js; \ - else \ - echo "No minify script found"; \ - fi - -.PHONY: build -build: clean validate ## Build package ZIP containing all sub-extensions - @echo "$(COLOR_BLUE)Building Joomla package...$(COLOR_RESET)" - @mkdir -p $(DIST_DIR) $(BUILD_DIR)/pkg_$(EXTENSION_NAME) - @# Build each sub-extension into its own ZIP - @for ext in src/packages/*/; do \ - EXT_NAME=$$(basename $$ext); \ - echo " Packaging $$EXT_NAME..."; \ - mkdir -p $(BUILD_DIR)/$$EXT_NAME; \ - rsync -a --exclude='.git*' "$$ext" "$(BUILD_DIR)/$$EXT_NAME/"; \ - cd $(BUILD_DIR) && $(ZIP) -r "pkg_$(EXTENSION_NAME)/$$EXT_NAME.zip" "$$EXT_NAME" && cd ..; \ - done - @# Copy the package manifest - @cp src/pkg_mokojoomstorelocator.xml $(BUILD_DIR)/pkg_$(EXTENSION_NAME)/ - @if [ -f "src/script.php" ]; then cp src/script.php $(BUILD_DIR)/pkg_$(EXTENSION_NAME)/; fi - @# Create the final package ZIP - @cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/pkg_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip" "pkg_$(EXTENSION_NAME)" - @echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/pkg_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip$(COLOR_RESET)" - -.PHONY: release -release: validate build ## Create a release (validate + build) - @echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)" - -.PHONY: version -version: ## Display version information - @echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)" - @echo " Name: $(EXTENSION_NAME)" - @echo " Type: $(EXTENSION_TYPE)" - @echo " Version: $(EXTENSION_VERSION)" - -# Default target -.DEFAULT_GOAL := help diff --git a/README.md b/README.md index 585b236..89fefb4 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,54 @@ -# MokoJoomStoreLocator +# MokoSuiteStoreLocator A Joomla 4/5 package providing a store locator listing component with coordinating map and search modules. ## Package Contents -| Extension | Description | -|---|---| -| `com_mokojoomstorelocator` | Component for managing store locations (admin CRUD + frontend listing) | -| `mod_mokojoomstorelocator_map` | Site module displaying an interactive map with location markers | -| `mod_mokojoomstorelocator_search` | Site module providing search/filter form for finding locations | +| Extension | Type | Element | +|---|---|---| +| Store Locator Component | component | `com_mokosuitestorelocator` | +| Store Locator Map | module (site) | `mod_mokosuitestorelocator_map` | +| Store Locator Search | module (site) | `mod_mokosuitestorelocator_search` | ## Requirements -- Joomla 4.4+ or 5.x -- PHP 8.1+ -- MySQL 5.7+ / MariaDB 10.3+ +- Joomla 5.x or 6.x +- PHP 8.2+ +- MySQL 8.0+ / MariaDB 10.4+ ## Installation -1. Download the latest `pkg_mokojoomstorelocator-x.x.x.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomStoreLocator/releases) +1. Download the latest `pkg_mokosuitestorelocator-x.x.x.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteStoreLocator/releases) 2. In Joomla Administrator, go to **System > Install > Extensions** 3. Upload the package ZIP — all extensions install automatically ## Features -- Manage store locations with address, coordinates, contact info, and business hours -- Interactive map display (OpenStreetMap/Leaflet or Google Maps) -- Location search by city, postcode, or radius -- Schema.org LocalBusiness structured data markup -- Category support for grouping locations +### Implemented +- **Admin CRUD** — full location management with tabbed edit form (details, address, coordinates, contact, image) +- **Admin list** — searchable, filterable, sortable locations list with bulk publish/unpublish/delete +- **Site frontend** — locations list and detail views with pagination +- **Schema.org** — LocalBusiness structured data markup on all frontend templates +- **SEF URLs** — router with menu, standard, and nomenu rules +- **Menu items** — "All Locations" list and single "Location Detail" picker +- **Interactive map** — Leaflet.js with OpenStreetMap tiles, markers with popups, auto-fit bounds +- **Location search** — city dropdown, radius filter, and browser geolocation ("Use My Location") + +### Planned +- Proximity search (Haversine distance filtering) +- Marker clustering for dense location areas +- Multi-category support with custom map markers +- ACL permissions and SQL upgrade schema +- REST API via Joomla Web Services plugin +- MokoSuiteShop integration for multi-store ecommerce + +## Development + +```bash +composer install # Install PHP dev dependencies +``` + +Source code lives in `source/packages/` — one directory per sub-extension. ## License diff --git a/source/packages/com_mokosuitestorelocator/admin/forms/filter_locations.xml b/source/packages/com_mokosuitestorelocator/admin/forms/filter_locations.xml new file mode 100644 index 0000000..6e36331 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/forms/filter_locations.xml @@ -0,0 +1,48 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + +
diff --git a/source/packages/com_mokosuitestorelocator/admin/forms/location.xml b/source/packages/com_mokosuitestorelocator/admin/forms/location.xml new file mode 100644 index 0000000..cb7cb2f --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/forms/location.xml @@ -0,0 +1,140 @@ + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + +
+ +
+ + + + + + + +
+ +
+ +
+
diff --git a/source/packages/com_mokosuitestorelocator/admin/language/en-GB/com_mokosuitestorelocator.ini b/source/packages/com_mokosuitestorelocator/admin/language/en-GB/com_mokosuitestorelocator.ini new file mode 100644 index 0000000..3b64378 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/language/en-GB/com_mokosuitestorelocator.ini @@ -0,0 +1,43 @@ +; MokoSuiteStoreLocator - Admin language strings +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GNU General Public License version 3 or later; see LICENSE + +COM_MOKOJOOMSTORELOCATOR="Store Locator" +COM_MOKOJOOMSTORELOCATOR_DESC="A store locator component for managing and displaying location listings." +COM_MOKOJOOMSTORELOCATOR_LOCATIONS="Locations" +COM_MOKOJOOMSTORELOCATOR_LOCATION_NEW="New Location" +COM_MOKOJOOMSTORELOCATOR_LOCATION_EDIT="Edit Location" +COM_MOKOJOOMSTORELOCATOR_TABLE_CAPTION="Store Location List" + +COM_MOKOJOOMSTORELOCATOR_CITY="City" +COM_MOKOJOOMSTORELOCATOR_STATE="State" + +COM_MOKOJOOMSTORELOCATOR_FIELDSET_ADDRESS="Address" +COM_MOKOJOOMSTORELOCATOR_FIELDSET_COORDINATES="Coordinates" +COM_MOKOJOOMSTORELOCATOR_FIELDSET_CONTACT="Contact Information" +COM_MOKOJOOMSTORELOCATOR_FIELDSET_IMAGE="Image" + +COM_MOKOJOOMSTORELOCATOR_FIELD_ADDRESS="Street Address" +COM_MOKOJOOMSTORELOCATOR_FIELD_CITY="City" +COM_MOKOJOOMSTORELOCATOR_FIELD_STATE="State / Province" +COM_MOKOJOOMSTORELOCATOR_FIELD_POSTCODE="Postal Code" +COM_MOKOJOOMSTORELOCATOR_FIELD_COUNTRY="Country" +COM_MOKOJOOMSTORELOCATOR_FIELD_LATITUDE="Latitude" +COM_MOKOJOOMSTORELOCATOR_FIELD_LONGITUDE="Longitude" +COM_MOKOJOOMSTORELOCATOR_FIELD_PHONE="Phone" +COM_MOKOJOOMSTORELOCATOR_FIELD_WEBSITE="Website" +COM_MOKOJOOMSTORELOCATOR_FIELD_HOURS="Business Hours" +COM_MOKOJOOMSTORELOCATOR_FIELD_IMAGE="Location Image" + +COM_MOKOJOOMSTORELOCATOR_FILTER_SEARCH_LABEL="Search Locations" +COM_MOKOJOOMSTORELOCATOR_CITY_ASC="City ascending" +COM_MOKOJOOMSTORELOCATOR_CITY_DESC="City descending" + +COM_MOKOJOOMSTORELOCATOR_LOCATION_SAVE_SUCCESS="Location saved successfully." +COM_MOKOJOOMSTORELOCATOR_LOCATIONS_N_ITEMS_PUBLISHED="%d location(s) published." +COM_MOKOJOOMSTORELOCATOR_LOCATIONS_N_ITEMS_UNPUBLISHED="%d location(s) unpublished." +COM_MOKOJOOMSTORELOCATOR_LOCATIONS_N_ITEMS_DELETED="%d location(s) deleted." + +COM_MOKOJOOMSTORELOCATOR_ERROR_TITLE_REQUIRED="A location title is required." +COM_MOKOJOOMSTORELOCATOR_ERROR_LATITUDE_RANGE="Latitude must be between -90 and 90." +COM_MOKOJOOMSTORELOCATOR_ERROR_LONGITUDE_RANGE="Longitude must be between -180 and 180." diff --git a/source/packages/com_mokosuitestorelocator/admin/language/en-GB/com_mokosuitestorelocator.sys.ini b/source/packages/com_mokosuitestorelocator/admin/language/en-GB/com_mokosuitestorelocator.sys.ini new file mode 100644 index 0000000..fa1f199 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/language/en-GB/com_mokosuitestorelocator.sys.ini @@ -0,0 +1,7 @@ +; MokoSuiteStoreLocator - System language strings +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GNU General Public License version 3 or later; see LICENSE + +COM_MOKOJOOMSTORELOCATOR="Store Locator" +COM_MOKOJOOMSTORELOCATOR_DESC="A store locator component for managing and displaying location listings." +COM_MOKOJOOMSTORELOCATOR_LOCATIONS="Locations" diff --git a/source/packages/com_mokosuitestorelocator/admin/services/provider.php b/source/packages/com_mokosuitestorelocator/admin/services/provider.php new file mode 100644 index 0000000..3008ab5 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/services/provider.php @@ -0,0 +1,57 @@ +registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoSuiteStoreLocator')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoSuiteStoreLocator')); + $container->registerServiceProvider(new RouterFactory('\\Moko\\Component\\MokoSuiteStoreLocator')); + + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MokoSuiteStoreLocatorComponent( + $container->get(ComponentDispatcherFactoryInterface::class) + ); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + + return $component; + } + ); + } +}; diff --git a/source/packages/com_mokosuitestorelocator/admin/sql/install.mysql.sql b/source/packages/com_mokosuitestorelocator/admin/sql/install.mysql.sql new file mode 100644 index 0000000..40a118c --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/sql/install.mysql.sql @@ -0,0 +1,40 @@ +-- ========================================================================= +-- Copyright (C) 2026 Moko Consulting +-- SPDX-License-Identifier: GPL-3.0-or-later +-- +-- MokoSuiteStoreLocator - Store locations table +-- ========================================================================= + +CREATE TABLE IF NOT EXISTS `#__mokosuitestorelocator_locations` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL DEFAULT '', + `alias` varchar(400) NOT NULL DEFAULT '', + `description` text NOT NULL, + `address` varchar(255) NOT NULL DEFAULT '', + `city` varchar(100) NOT NULL DEFAULT '', + `state` varchar(100) NOT NULL DEFAULT '', + `postcode` varchar(20) NOT NULL DEFAULT '', + `country` varchar(100) NOT NULL DEFAULT '', + `latitude` decimal(10, 8) DEFAULT NULL, + `longitude` decimal(11, 8) DEFAULT NULL, + `phone` varchar(50) NOT NULL DEFAULT '', + `email` varchar(255) NOT NULL DEFAULT '', + `website` varchar(255) NOT NULL DEFAULT '', + `hours` text NOT NULL, + `image` varchar(255) NOT NULL DEFAULT '', + `published` tinyint(4) NOT NULL DEFAULT 0, + `ordering` int(11) NOT NULL DEFAULT 0, + `catid` int(11) NOT NULL DEFAULT 0, + `params` text NOT NULL, + `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `created_by` int(10) unsigned NOT NULL DEFAULT 0, + `modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `modified_by` int(10) unsigned NOT NULL DEFAULT 0, + `checked_out` int(10) unsigned DEFAULT NULL, + `checked_out_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_published` (`published`), + KEY `idx_catid` (`catid`), + KEY `idx_alias` (`alias`(191)), + KEY `idx_coordinates` (`latitude`, `longitude`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/source/packages/com_mokosuitestorelocator/admin/sql/uninstall.mysql.sql b/source/packages/com_mokosuitestorelocator/admin/sql/uninstall.mysql.sql new file mode 100644 index 0000000..9b5cc55 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/sql/uninstall.mysql.sql @@ -0,0 +1,6 @@ +-- ========================================================================= +-- Copyright (C) 2026 Moko Consulting +-- SPDX-License-Identifier: GPL-3.0-or-later +-- ========================================================================= + +DROP TABLE IF EXISTS `#__mokosuitestorelocator_locations`; diff --git a/source/packages/com_mokosuitestorelocator/admin/src/Controller/DisplayController.php b/source/packages/com_mokosuitestorelocator/admin/src/Controller/DisplayController.php new file mode 100644 index 0000000..97b9c17 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/src/Controller/DisplayController.php @@ -0,0 +1,29 @@ + true]) + { + return parent::getModel($name, $prefix, $config); + } +} diff --git a/source/packages/com_mokosuitestorelocator/admin/src/Extension/MokoSuiteStoreLocatorComponent.php b/source/packages/com_mokosuitestorelocator/admin/src/Extension/MokoSuiteStoreLocatorComponent.php new file mode 100644 index 0000000..2238b9f --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/src/Extension/MokoSuiteStoreLocatorComponent.php @@ -0,0 +1,25 @@ +loadForm( + 'com_mokosuitestorelocator.location', + 'location', + ['control' => 'jform', 'load_data' => $loadData] + ); + + if (empty($form)) + { + return false; + } + + return $form; + } + + /** + * Load the data for the form. + * + * @return mixed The data for the form. + * + * @since 1.0.0 + */ + protected function loadFormData() + { + $data = $this->getItem(); + + return $data; + } + + /** + * Get the table for this model. + * + * @param string $name The table name. + * @param string $prefix The table prefix. + * @param array $options Configuration array for the table. + * + * @return Table + * + * @since 1.0.0 + */ + public function getTable($name = 'Location', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } +} diff --git a/source/packages/com_mokosuitestorelocator/admin/src/Model/LocationsModel.php b/source/packages/com_mokosuitestorelocator/admin/src/Model/LocationsModel.php new file mode 100644 index 0000000..d756a14 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/src/Model/LocationsModel.php @@ -0,0 +1,121 @@ +getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string'); + $this->setState('filter.search', $search); + + $published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', '', 'string'); + $this->setState('filter.published', $published); + + parent::populateState($ordering, $direction); + } + + /** + * Build an SQL query to load the list data. + * + * @return QueryInterface + * + * @since 1.0.0 + */ + protected function getListQuery(): QueryInterface + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select('a.*') + ->from($db->quoteName('#__mokosuitestorelocator_locations', 'a')); + + // Filter by published state + $published = $this->getState('filter.published'); + + if (is_numeric($published)) + { + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, \Joomla\Database\ParameterType::INTEGER); + } + elseif ($published === '') + { + $query->where($db->quoteName('a.published') . ' IN (0, 1)'); + } + + // Search filter + $search = $this->getState('filter.search'); + + if (!empty($search)) + { + $search = '%' . trim($search) . '%'; + $query->where( + '(' . $db->quoteName('a.title') . ' LIKE :search' + . ' OR ' . $db->quoteName('a.city') . ' LIKE :search2' + . ' OR ' . $db->quoteName('a.state') . ' LIKE :search3' + . ' OR ' . $db->quoteName('a.address') . ' LIKE :search4)' + ) + ->bind(':search', $search) + ->bind(':search2', $search) + ->bind(':search3', $search) + ->bind(':search4', $search); + } + + // Ordering + $orderCol = $this->state->get('list.ordering', 'a.title'); + $orderDir = $this->state->get('list.direction', 'ASC'); + $query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir)); + + return $query; + } +} diff --git a/source/packages/com_mokosuitestorelocator/admin/src/Table/LocationTable.php b/source/packages/com_mokosuitestorelocator/admin/src/Table/LocationTable.php new file mode 100644 index 0000000..fb486cc --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/src/Table/LocationTable.php @@ -0,0 +1,98 @@ +setColumnAlias('published', 'published'); + } + + /** + * Overloaded check method to ensure data integrity. + * + * @return boolean True if the data is valid. + * + * @since 1.0.0 + */ + public function check(): bool + { + if (trim($this->title) === '') + { + $this->setError(Text::_('COM_MOKOJOOMSTORELOCATOR_ERROR_TITLE_REQUIRED')); + + return false; + } + + if (trim($this->alias) === '') + { + $this->alias = $this->title; + } + + $this->alias = OutputFilter::stringURLSafe($this->alias); + + if ($this->latitude !== null && ($this->latitude < -90 || $this->latitude > 90)) + { + $this->setError(Text::_('COM_MOKOJOOMSTORELOCATOR_ERROR_LATITUDE_RANGE')); + + return false; + } + + if ($this->longitude !== null && ($this->longitude < -180 || $this->longitude > 180)) + { + $this->setError(Text::_('COM_MOKOJOOMSTORELOCATOR_ERROR_LONGITUDE_RANGE')); + + return false; + } + + $now = Factory::getDate()->toSql(); + $user = Factory::getApplication()->getIdentity(); + + if (!(int) $this->id) + { + if (!$this->created || $this->created === '0000-00-00 00:00:00') + { + $this->created = $now; + } + + if (!$this->created_by) + { + $this->created_by = $user->id; + } + } + + $this->modified = $now; + $this->modified_by = $user->id; + + return parent::check(); + } +} diff --git a/source/packages/com_mokosuitestorelocator/admin/src/View/Location/HtmlView.php b/source/packages/com_mokosuitestorelocator/admin/src/View/Location/HtmlView.php new file mode 100644 index 0000000..f8208a0 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/src/View/Location/HtmlView.php @@ -0,0 +1,89 @@ +form = $this->get('Form'); + $this->item = $this->get('Item'); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.0.0 + */ + protected function addToolbar(): void + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + + ToolbarHelper::title( + Text::_('COM_MOKOJOOMSTORELOCATOR_LOCATION_' . ($isNew ? 'NEW' : 'EDIT')), + 'location' + ); + + ToolbarHelper::apply('location.apply'); + ToolbarHelper::save('location.save'); + ToolbarHelper::save2new('location.save2new'); + + if (!$isNew) + { + ToolbarHelper::save2copy('location.save2copy'); + } + + ToolbarHelper::cancel('location.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE'); + } +} diff --git a/source/packages/com_mokosuitestorelocator/admin/src/View/Locations/HtmlView.php b/source/packages/com_mokosuitestorelocator/admin/src/View/Locations/HtmlView.php new file mode 100644 index 0000000..eaf52f9 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/src/View/Locations/HtmlView.php @@ -0,0 +1,91 @@ +items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.0.0 + */ + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_MOKOJOOMSTORELOCATOR_LOCATIONS'), 'location'); + ToolbarHelper::addNew('location.add'); + ToolbarHelper::publish('locations.publish', 'JTOOLBAR_PUBLISH', true); + ToolbarHelper::unpublish('locations.unpublish', 'JTOOLBAR_UNPUBLISH', true); + ToolbarHelper::deleteList('', 'locations.delete', 'JTOOLBAR_DELETE'); + } +} diff --git a/source/packages/com_mokosuitestorelocator/admin/tmpl/location/edit.php b/source/packages/com_mokosuitestorelocator/admin/tmpl/location/edit.php new file mode 100644 index 0000000..d8aec50 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/tmpl/location/edit.php @@ -0,0 +1,73 @@ + +
+ + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+
+ form->renderField('title'); ?> + form->renderField('alias'); ?> + form->renderField('description'); ?> +
+
+ form->renderField('published'); ?> + form->renderField('image'); ?> +
+
+ + + +
+
+ form->renderField('address'); ?> + form->renderField('city'); ?> + form->renderField('state'); ?> + form->renderField('postcode'); ?> + form->renderField('country'); ?> +
+
+ form->renderField('latitude'); ?> + form->renderField('longitude'); ?> +
+
+ + + +
+
+ form->renderField('phone'); ?> + form->renderField('email'); ?> + form->renderField('website'); ?> +
+
+ form->renderField('hours'); ?> +
+
+ + + + + + +
diff --git a/source/packages/com_mokosuitestorelocator/admin/tmpl/locations/default.php b/source/packages/com_mokosuitestorelocator/admin/tmpl/locations/default.php new file mode 100644 index 0000000..c00941f --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/admin/tmpl/locations/default.php @@ -0,0 +1,98 @@ + +
+ +
+
+
+ $this]); ?> + + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + escape($item->title); ?> + + alias) : ?> +
escape($item->alias); ?>
+ +
+ escape($item->city); ?> + + escape($item->state); ?> + + published, $i, 'locations.', true, 'cb'); ?> + + id; ?> +
+ + pagination->getListFooter(); ?> + + + + + +
+
+
+
diff --git a/source/packages/com_mokosuitestorelocator/mokosuitestorelocator.xml b/source/packages/com_mokosuitestorelocator/mokosuitestorelocator.xml new file mode 100644 index 0000000..f882ef5 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/mokosuitestorelocator.xml @@ -0,0 +1,62 @@ + + + + com_mokosuitestorelocator + 1.0.0 + 2026-06-23 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GNU General Public License version 3 or later; see LICENSE + COM_MOKOJOOMSTORELOCATOR_DESC + + Moko\Component\MokoSuiteStoreLocator + + + + sql/install.mysql.sql + + + + + + sql/uninstall.mysql.sql + + + + + language + src + tmpl + + + + + forms + language + services + sql + src + tmpl + + + COM_MOKOJOOMSTORELOCATOR + + COM_MOKOJOOMSTORELOCATOR_LOCATIONS + + + diff --git a/source/packages/com_mokosuitestorelocator/site/language/en-GB/com_mokosuitestorelocator.ini b/source/packages/com_mokosuitestorelocator/site/language/en-GB/com_mokosuitestorelocator.ini new file mode 100644 index 0000000..00eb1fb --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/site/language/en-GB/com_mokosuitestorelocator.ini @@ -0,0 +1,19 @@ +; MokoSuiteStoreLocator - Site language strings +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GNU General Public License version 3 or later; see LICENSE + +COM_MOKOJOOMSTORELOCATOR="Store Locator" +COM_MOKOJOOMSTORELOCATOR_LOCATIONS="Locations" +COM_MOKOJOOMSTORELOCATOR_NO_LOCATIONS="No locations found." + +COM_MOKOJOOMSTORELOCATOR_FIELDSET_ADDRESS="Address" +COM_MOKOJOOMSTORELOCATOR_FIELDSET_CONTACT="Contact Information" +COM_MOKOJOOMSTORELOCATOR_FIELD_PHONE="Phone" +COM_MOKOJOOMSTORELOCATOR_FIELD_WEBSITE="Website" +COM_MOKOJOOMSTORELOCATOR_FIELD_HOURS="Business Hours" + +COM_MOKOJOOMSTORELOCATOR_LOCATIONS_VIEW_DEFAULT_TITLE="All Locations" +COM_MOKOJOOMSTORELOCATOR_LOCATIONS_VIEW_DEFAULT_DESC="Displays a list of all store locations." +COM_MOKOJOOMSTORELOCATOR_LOCATION_VIEW_DEFAULT_TITLE="Location Detail" +COM_MOKOJOOMSTORELOCATOR_LOCATION_VIEW_DEFAULT_DESC="Displays a single store location with full details." +COM_MOKOJOOMSTORELOCATOR_FIELD_LOCATION="Select Location" diff --git a/source/packages/com_mokosuitestorelocator/site/src/Controller/DisplayController.php b/source/packages/com_mokosuitestorelocator/site/src/Controller/DisplayController.php new file mode 100644 index 0000000..313002a --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/site/src/Controller/DisplayController.php @@ -0,0 +1,29 @@ +getState('location.id'); + + if ($this->_item === null) + { + $this->_item = []; + } + + if (!isset($this->_item[$pk])) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select('a.*') + ->from($db->quoteName('#__mokosuitestorelocator_locations', 'a')) + ->where($db->quoteName('a.id') . ' = :pk') + ->where($db->quoteName('a.published') . ' = 1') + ->bind(':pk', $pk, ParameterType::INTEGER); + + $db->setQuery($query); + $this->_item[$pk] = $db->loadObject(); + } + + return $this->_item[$pk] ?? null; + } + + /** + * Populate the model state. + * + * @return void + * + * @since 1.0.0 + */ + protected function populateState() + { + $app = $this->getApplication(); + + $id = $app->input->getInt('id', 0); + $this->setState('location.id', $id); + } +} diff --git a/source/packages/com_mokosuitestorelocator/site/src/Model/LocationsModel.php b/source/packages/com_mokosuitestorelocator/site/src/Model/LocationsModel.php new file mode 100644 index 0000000..168e680 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/site/src/Model/LocationsModel.php @@ -0,0 +1,132 @@ +getApplication(); + + $search = $app->input->getString('search', ''); + $this->setState('filter.search', $search); + + $city = $app->input->getString('city', ''); + $this->setState('filter.city', $city); + + $state = $app->input->getString('state', ''); + $this->setState('filter.state', $state); + + parent::populateState($ordering, $direction); + } + + /** + * Build the query for the locations list. + * + * @return QueryInterface + * + * @since 1.0.0 + */ + protected function getListQuery(): QueryInterface + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select('a.*') + ->from($db->quoteName('#__mokosuitestorelocator_locations', 'a')) + ->where($db->quoteName('a.published') . ' = 1'); + + // Search filter + $search = $this->getState('filter.search'); + + if (!empty($search)) + { + $search = '%' . trim($search) . '%'; + $query->where( + '(' . $db->quoteName('a.title') . ' LIKE :search' + . ' OR ' . $db->quoteName('a.city') . ' LIKE :search2' + . ' OR ' . $db->quoteName('a.state') . ' LIKE :search3' + . ' OR ' . $db->quoteName('a.address') . ' LIKE :search4)' + ) + ->bind(':search', $search) + ->bind(':search2', $search) + ->bind(':search3', $search) + ->bind(':search4', $search); + } + + // City filter + $city = $this->getState('filter.city'); + + if (!empty($city)) + { + $query->where($db->quoteName('a.city') . ' = :city') + ->bind(':city', $city); + } + + // State filter + $state = $this->getState('filter.state'); + + if (!empty($state)) + { + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state); + } + + // Ordering + $orderCol = $this->state->get('list.ordering', 'a.ordering'); + $orderDir = $this->state->get('list.direction', 'ASC'); + $query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir)); + + return $query; + } +} diff --git a/source/packages/com_mokosuitestorelocator/site/src/Service/Router.php b/source/packages/com_mokosuitestorelocator/site/src/Service/Router.php new file mode 100644 index 0000000..a7fff3f --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/site/src/Service/Router.php @@ -0,0 +1,51 @@ +registerView($locations); + + $location = new RouterViewConfiguration('location'); + $location->setKey('id')->setParent($locations); + $this->registerView($location); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } +} diff --git a/source/packages/com_mokosuitestorelocator/site/src/View/Location/HtmlView.php b/source/packages/com_mokosuitestorelocator/site/src/View/Location/HtmlView.php new file mode 100644 index 0000000..91d17d2 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/site/src/View/Location/HtmlView.php @@ -0,0 +1,48 @@ +item = $this->get('Item'); + + if ($this->item === null) + { + throw new \Exception('Location not found', 404); + } + + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitestorelocator/site/src/View/Locations/HtmlView.php b/source/packages/com_mokosuitestorelocator/site/src/View/Locations/HtmlView.php new file mode 100644 index 0000000..dff9a73 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/site/src/View/Locations/HtmlView.php @@ -0,0 +1,57 @@ +items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitestorelocator/site/tmpl/location/default.php b/source/packages/com_mokosuitestorelocator/site/tmpl/location/default.php new file mode 100644 index 0000000..9530029 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/site/tmpl/location/default.php @@ -0,0 +1,105 @@ +item; +?> +
+

escape($item->title); ?>

+ + image) : ?> +
+ <?php echo $this->escape($item->title); ?> +
+ + + description) : ?> +
+ description); ?> +
+ + +
+
+

+ address) : ?> + escape($item->address); ?>
+ + city) : ?> + escape($item->city); ?>, + + state) : ?> + escape($item->state); ?> + + postcode) : ?> + escape($item->postcode); ?> + + country) : ?> +
escape($item->country); ?> + +
+ +
+

+ phone) : ?> + + + + email) : ?> + + + + website) : ?> + + +
+ + hours) : ?> +
+

+
+ escape($item->hours)); ?> +
+
+ +
+ + latitude && $item->longitude) : ?> + + +
+
+ +
diff --git a/source/packages/com_mokosuitestorelocator/site/tmpl/location/default.xml b/source/packages/com_mokosuitestorelocator/site/tmpl/location/default.xml new file mode 100644 index 0000000..8d500fd --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/site/tmpl/location/default.xml @@ -0,0 +1,22 @@ + + + + + + + + +
+ +
+
+
diff --git a/source/packages/com_mokosuitestorelocator/site/tmpl/locations/default.php b/source/packages/com_mokosuitestorelocator/site/tmpl/locations/default.php new file mode 100644 index 0000000..2732468 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/site/tmpl/locations/default.php @@ -0,0 +1,69 @@ + +
+

+ + items)) : ?> +

+ +
+ items as $item) : ?> +
+ image) : ?> +
+ <?php echo $this->escape($item->title); ?> +
+ + +
+

+ + escape($item->title); ?> + +

+ +
+ address) : ?> + escape($item->address); ?>
+ + city) : ?> + escape($item->city); ?>, + + state) : ?> + escape($item->state); ?> + + postcode) : ?> + escape($item->postcode); ?> + +
+ + phone) : ?> + + +
+
+ +
+ + pagination->getListFooter(); ?> + +
diff --git a/source/packages/com_mokosuitestorelocator/site/tmpl/locations/default.xml b/source/packages/com_mokosuitestorelocator/site/tmpl/locations/default.xml new file mode 100644 index 0000000..dc34b45 --- /dev/null +++ b/source/packages/com_mokosuitestorelocator/site/tmpl/locations/default.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/source/packages/mod_mokosuitestorelocator_map/language/en-GB/mod_mokosuitestorelocator_map.ini b/source/packages/mod_mokosuitestorelocator_map/language/en-GB/mod_mokosuitestorelocator_map.ini new file mode 100644 index 0000000..081c77d --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_map/language/en-GB/mod_mokosuitestorelocator_map.ini @@ -0,0 +1,12 @@ +; MokoSuiteStoreLocator Map Module - Language strings +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GNU General Public License version 3 or later; see LICENSE + +MOD_MOKOJOOMSTORELOCATOR_MAP="Store Locator Map" +MOD_MOKOJOOMSTORELOCATOR_MAP_DESC="Displays an interactive map with store location markers." +MOD_MOKOJOOMSTORELOCATOR_MAP_HEIGHT="Map Height" +MOD_MOKOJOOMSTORELOCATOR_MAP_ZOOM="Default Zoom Level" +MOD_MOKOJOOMSTORELOCATOR_MAP_PROVIDER="Map Provider" +MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY="API Key" +MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY_DESC="Required for Google Maps. Not needed for OpenStreetMap." +MOD_MOKOJOOMSTORELOCATOR_MAP_NOSCRIPT="JavaScript is required to display the map." diff --git a/source/packages/mod_mokosuitestorelocator_map/language/en-GB/mod_mokosuitestorelocator_map.sys.ini b/source/packages/mod_mokosuitestorelocator_map/language/en-GB/mod_mokosuitestorelocator_map.sys.ini new file mode 100644 index 0000000..ab9924f --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_map/language/en-GB/mod_mokosuitestorelocator_map.sys.ini @@ -0,0 +1,6 @@ +; MokoSuiteStoreLocator Map Module - System language strings +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GNU General Public License version 3 or later; see LICENSE + +MOD_MOKOJOOMSTORELOCATOR_MAP="Store Locator Map" +MOD_MOKOJOOMSTORELOCATOR_MAP_DESC="Displays an interactive map with store location markers." diff --git a/source/packages/mod_mokosuitestorelocator_map/mod_mokosuitestorelocator_map.xml b/source/packages/mod_mokosuitestorelocator_map/mod_mokosuitestorelocator_map.xml new file mode 100644 index 0000000..25a83c0 --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_map/mod_mokosuitestorelocator_map.xml @@ -0,0 +1,72 @@ + + + + mod_mokosuitestorelocator_map + 1.0.0 + 2026-06-23 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GNU General Public License version 3 or later; see LICENSE + MOD_MOKOJOOMSTORELOCATOR_MAP_DESC + + Moko\Module\MokoSuiteStoreLocatorMap + + + src + tmpl + language + + + + +
+ + + + + + + + + + +
+
+
+
diff --git a/source/packages/mod_mokosuitestorelocator_map/src/Dispatcher/Dispatcher.php b/source/packages/mod_mokosuitestorelocator_map/src/Dispatcher/Dispatcher.php new file mode 100644 index 0000000..4b3c131 --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_map/src/Dispatcher/Dispatcher.php @@ -0,0 +1,80 @@ +getDatabase(); + $query = $db->getQuery(true); + + $query->select([ + $db->quoteName('id'), + $db->quoteName('title'), + $db->quoteName('address'), + $db->quoteName('city'), + $db->quoteName('state'), + $db->quoteName('postcode'), + $db->quoteName('phone'), + $db->quoteName('latitude'), + $db->quoteName('longitude'), + ]) + ->from($db->quoteName('#__mokosuitestorelocator_locations')) + ->where($db->quoteName('published') . ' = 1') + ->where($db->quoteName('latitude') . ' IS NOT NULL') + ->where($db->quoteName('longitude') . ' IS NOT NULL'); + + $db->setQuery($query); + $locations = $db->loadObjectList() ?: []; + + $markers = []; + + foreach ($locations as $loc) + { + $markers[] = [ + 'id' => (int) $loc->id, + 'title' => $loc->title, + 'address' => trim($loc->address . ', ' . $loc->city . ', ' . $loc->state . ' ' . $loc->postcode, ', '), + 'phone' => $loc->phone, + 'lat' => (float) $loc->latitude, + 'lng' => (float) $loc->longitude, + ]; + } + + $data['locations'] = $markers; + + return $data; + } +} diff --git a/source/packages/mod_mokosuitestorelocator_map/tmpl/default.php b/source/packages/mod_mokosuitestorelocator_map/tmpl/default.php new file mode 100644 index 0000000..d05fa24 --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_map/tmpl/default.php @@ -0,0 +1,82 @@ +id; +$mapHeight = $params->get('map_height', '400px'); +$mapZoom = (int) $params->get('map_zoom', 10); +$provider = $params->get('map_provider', 'leaflet'); +$apiKey = $params->get('api_key', ''); + +/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ +$wa = $displayData['app']->getDocument()->getWebAssetManager(); + +if ($provider === 'leaflet') +{ + $wa->registerAndUseStyle('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', [], ['integrity' => 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=', 'crossorigin' => '']); + $wa->registerAndUseScript('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], ['integrity' => 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=', 'crossorigin' => '', 'defer' => true]); +} +?> +
data-api-key="escape($apiKey); ?>"> + +
+ + diff --git a/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.ini b/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.ini new file mode 100644 index 0000000..fb04db5 --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.ini @@ -0,0 +1,20 @@ +; MokoSuiteStoreLocator Search Module - Language strings +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GNU General Public License version 3 or later; see LICENSE + +MOD_MOKOJOOMSTORELOCATOR_SEARCH="Store Locator Search" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_DESC="Provides a search/filter form for finding store locations." +MOD_MOKOJOOMSTORELOCATOR_SEARCH_LABEL="Find a Store" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_PLACEHOLDER="Enter city, postcode, or address..." +MOD_MOKOJOOMSTORELOCATOR_SEARCH_SHOW_CITY="Show City Filter" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_SHOW_RADIUS="Show Radius Filter" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS_UNIT="Distance Unit" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS_OPTIONS="Radius Options" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS_OPTIONS_DESC="Comma-separated list of radius values (e.g., 5,10,25,50,100)" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_CITY="City" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_ALL_CITIES="All Cities" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS="Distance" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_ANY_DISTANCE="Any Distance" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_USE_LOCATION="Use My Location" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_LOCATING="Locating..." +MOD_MOKOJOOMSTORELOCATOR_SEARCH_LOCATION_SET="Location Set" diff --git a/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.sys.ini b/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.sys.ini new file mode 100644 index 0000000..5400537 --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_search/language/en-GB/mod_mokosuitestorelocator_search.sys.ini @@ -0,0 +1,6 @@ +; MokoSuiteStoreLocator Search Module - System language strings +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GNU General Public License version 3 or later; see LICENSE + +MOD_MOKOJOOMSTORELOCATOR_SEARCH="Store Locator Search" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_DESC="Provides a search/filter form for finding store locations." diff --git a/source/packages/mod_mokosuitestorelocator_search/mod_mokosuitestorelocator_search.xml b/source/packages/mod_mokosuitestorelocator_search/mod_mokosuitestorelocator_search.xml new file mode 100644 index 0000000..5594d77 --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_search/mod_mokosuitestorelocator_search.xml @@ -0,0 +1,79 @@ + + + + mod_mokosuitestorelocator_search + 1.0.0 + 2026-06-23 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GNU General Public License version 3 or later; see LICENSE + MOD_MOKOJOOMSTORELOCATOR_SEARCH_DESC + + Moko\Module\MokoSuiteStoreLocatorSearch + + + src + tmpl + language + + + + +
+ + + + + + + + + + + + + + + + +
+
+
+
diff --git a/source/packages/mod_mokosuitestorelocator_search/src/Dispatcher/Dispatcher.php b/source/packages/mod_mokosuitestorelocator_search/src/Dispatcher/Dispatcher.php new file mode 100644 index 0000000..59f9f78 --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_search/src/Dispatcher/Dispatcher.php @@ -0,0 +1,72 @@ +getDatabase(); + + // Load distinct cities + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('city')) + ->from($db->quoteName('#__mokosuitestorelocator_locations')) + ->where($db->quoteName('published') . ' = 1') + ->where($db->quoteName('city') . " != ''") + ->order($db->quoteName('city') . ' ASC'); + + $db->setQuery($query); + $data['cities'] = $db->loadColumn() ?: []; + + // Load distinct states + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('state')) + ->from($db->quoteName('#__mokosuitestorelocator_locations')) + ->where($db->quoteName('published') . ' = 1') + ->where($db->quoteName('state') . " != ''") + ->order($db->quoteName('state') . ' ASC'); + + $db->setQuery($query); + $data['states'] = $db->loadColumn() ?: []; + + // Build radius options from params + $radiusStr = $params->get('radius_options', '5,10,25,50,100'); + $data['radiusOptions'] = array_map('intval', array_filter(explode(',', $radiusStr))); + $data['radiusUnit'] = $params->get('radius_unit', 'miles'); + + return $data; + } +} diff --git a/source/packages/mod_mokosuitestorelocator_search/tmpl/default.php b/source/packages/mod_mokosuitestorelocator_search/tmpl/default.php new file mode 100644 index 0000000..3e21765 --- /dev/null +++ b/source/packages/mod_mokosuitestorelocator_search/tmpl/default.php @@ -0,0 +1,120 @@ +get('show_city_filter', 1); +$showRadius = (int) $params->get('show_radius_filter', 1); +$moduleId = $displayData['module']->id; +?> + + + + + diff --git a/source/pkg_mokosuitestorelocator.xml b/source/pkg_mokosuitestorelocator.xml new file mode 100644 index 0000000..a7d7569 --- /dev/null +++ b/source/pkg_mokosuitestorelocator.xml @@ -0,0 +1,42 @@ + + + + pkg_mokosuitestorelocator + mokosuitestorelocator + 1.0.0 + 2026-06-23 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GNU General Public License version 3 or later; see LICENSE + PKG_MOKOJOOMSTORELOCATOR_DESC + script.php + + + com_mokosuitestorelocator.zip + mod_mokosuitestorelocator_map.zip + mod_mokosuitestorelocator_search.zip + + + + https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteStoreLocator/updates.xml + + + true + diff --git a/source/script.php b/source/script.php new file mode 100644 index 0000000..0a77084 --- /dev/null +++ b/source/script.php @@ -0,0 +1,131 @@ +minimumPhp, '<')) + { + Log::add( + 'MokoSuiteStoreLocator requires PHP ' . $this->minimumPhp . ' or later.', + Log::WARNING, + 'jerror' + ); + + return false; + } + + if (version_compare(JVERSION, $this->minimumJoomla, '<')) + { + Log::add( + 'MokoSuiteStoreLocator requires Joomla ' . $this->minimumJoomla . ' or later.', + Log::WARNING, + 'jerror' + ); + + return false; + } + + return true; + } + + /** + * Called on installation. + * + * @param InstallerAdapter $parent The parent installer object. + * + * @return boolean True on success. + * + * @since 1.0.0 + */ + public function install(InstallerAdapter $parent): bool + { + return true; + } + + /** + * Called on update. + * + * @param InstallerAdapter $parent The parent installer object. + * + * @return boolean True on success. + * + * @since 1.0.0 + */ + public function update(InstallerAdapter $parent): bool + { + return true; + } + + /** + * Called on uninstallation. + * + * @param InstallerAdapter $parent The parent installer object. + * + * @return boolean True on success. + * + * @since 1.0.0 + */ + public function uninstall(InstallerAdapter $parent): bool + { + return true; + } + + /** + * Called after any type of action. + * + * @param string $type Installation type. + * @param InstallerAdapter $parent The parent installer object. + * + * @return boolean True on success. + * + * @since 1.0.0 + */ + public function postflight(string $type, InstallerAdapter $parent): bool + { + return true; + } +}