feat: v1.1 competitive parity — proximity, directions, geocoding, CSV import (#55)
This commit was merged in pull request #55.
This commit is contained in:
@@ -5,6 +5,21 @@ 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).
|
||||
|
||||
## [1.1.0] - Unreleased
|
||||
|
||||
### Added
|
||||
- Haversine proximity search — filter locations by distance from user's coordinates
|
||||
- Hidden `radius_unit` field in search module to pass miles/km preference to component
|
||||
- Distance-sorted results when proximity search is active
|
||||
- "Get Directions" link on location detail page (Google Maps, no API key needed)
|
||||
- "Get Directions" link in Leaflet map popup markers
|
||||
- Auto-geocoding on admin save — coordinates populated from address via Nominatim/OSM API
|
||||
- CSV import: upload CSV file to bulk-create locations
|
||||
- CSV import: auto-detect column headers (title/name/store, address/street, city, etc.)
|
||||
- CSV import: per-row validation via LocationTable::bind()->check()->store()
|
||||
- CSV import view accessible from admin toolbar and submenu
|
||||
- Language strings for directions, geocoding feedback, and import UI
|
||||
|
||||
## [1.0.0] - 2026-06-23
|
||||
|
||||
### Added
|
||||
|
||||
@@ -33,9 +33,12 @@ A Joomla 4/5 package providing a store locator listing component with coordinati
|
||||
- **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")
|
||||
- **Proximity search** — Haversine distance filtering with distance-sorted results
|
||||
- **Get Directions** — Google Maps directions link on detail page and map popups
|
||||
- **Auto-geocoding** — coordinates auto-populated from address on save (Nominatim/OSM)
|
||||
- **CSV import** — bulk-create locations from spreadsheet with auto-detected column mapping
|
||||
|
||||
### 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
|
||||
|
||||
+18
@@ -41,3 +41,21 @@ 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."
|
||||
|
||||
COM_MOKOJOOMSTORELOCATOR_GEOCODING_SUCCESS="Coordinates were auto-populated from the address via OpenStreetMap."
|
||||
COM_MOKOJOOMSTORELOCATOR_GEOCODING_FAILED="Geocoding failed: %s. You can enter coordinates manually."
|
||||
COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS="Get Directions"
|
||||
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT="Import Locations"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_DESC="Import store locations from a CSV file."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_UPLOAD="Upload CSV File"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FILE="CSV File"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FILE_DESC="Select a CSV file with location data. First row must be column headers."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_DELIMITER="Delimiter"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_DELIMITER_DESC="The character separating fields in your CSV file."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_SUCCESS="%d location(s) imported successfully."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_SKIPPED="%d row(s) skipped due to errors."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_NO_FILE="No file was uploaded."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_INVALID_FILE="The uploaded file is not a valid CSV."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_NO_ROWS="The CSV file contains no data rows."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_MISSING_TITLE="Row %d: Title is required."
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuiteStoreLocator
|
||||
* @subpackage com_mokosuitestorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
|
||||
/**
|
||||
* Import controller for CSV location uploads.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
class ImportController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Process the uploaded CSV file.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function import(): void
|
||||
{
|
||||
Session::checkToken() or jexit(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
// ACL check — user must have create permission
|
||||
if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokosuitestorelocator'))
|
||||
{
|
||||
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitestorelocator&view=locations', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Moko\Component\MokoSuiteStoreLocator\Administrator\Model\ImportModel $model */
|
||||
$model = $this->getModel('Import', 'Administrator');
|
||||
|
||||
$file = $this->input->files->get('jform', [], 'array');
|
||||
$delimiter = $this->input->post->getString('delimiter', ',');
|
||||
|
||||
$csvFile = $file['csv_file'] ?? null;
|
||||
|
||||
if (!$csvFile || $csvFile['error'] !== UPLOAD_ERR_OK || !is_uploaded_file($csvFile['tmp_name']))
|
||||
{
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_NO_FILE'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitestorelocator&view=import', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file extension
|
||||
$ext = strtolower(pathinfo($csvFile['name'], PATHINFO_EXTENSION));
|
||||
|
||||
if ($ext !== 'csv' && $ext !== 'txt')
|
||||
{
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_INVALID_FILE'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitestorelocator&view=import', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $model->processImport($csvFile['tmp_name'], $delimiter);
|
||||
|
||||
if ($result['imported'] > 0)
|
||||
{
|
||||
$this->setMessage(Text::sprintf('COM_MOKOJOOMSTORELOCATOR_IMPORT_SUCCESS', $result['imported']));
|
||||
}
|
||||
|
||||
if ($result['skipped'] > 0)
|
||||
{
|
||||
$this->setMessage(Text::sprintf('COM_MOKOJOOMSTORELOCATOR_IMPORT_SKIPPED', $result['skipped']), 'warning');
|
||||
}
|
||||
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitestorelocator&view=locations', false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuiteStoreLocator
|
||||
* @subpackage com_mokosuitestorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
use SplFileObject;
|
||||
|
||||
/**
|
||||
* Import model for CSV location processing.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
class ImportModel extends BaseDatabaseModel
|
||||
{
|
||||
/**
|
||||
* Known CSV column names mapped to database fields.
|
||||
*
|
||||
* @var array
|
||||
* @since 1.1.0
|
||||
*/
|
||||
private const COLUMN_MAP = [
|
||||
'title' => 'title',
|
||||
'name' => 'title',
|
||||
'store' => 'title',
|
||||
'location' => 'title',
|
||||
'description' => 'description',
|
||||
'address' => 'address',
|
||||
'street' => 'address',
|
||||
'city' => 'city',
|
||||
'state' => 'state',
|
||||
'province' => 'state',
|
||||
'region' => 'state',
|
||||
'postcode' => 'postcode',
|
||||
'zip' => 'postcode',
|
||||
'zipcode' => 'postcode',
|
||||
'postal_code' => 'postcode',
|
||||
'country' => 'country',
|
||||
'latitude' => 'latitude',
|
||||
'lat' => 'latitude',
|
||||
'longitude' => 'longitude',
|
||||
'lng' => 'longitude',
|
||||
'lon' => 'longitude',
|
||||
'phone' => 'phone',
|
||||
'telephone' => 'phone',
|
||||
'email' => 'email',
|
||||
'website' => 'website',
|
||||
'url' => 'website',
|
||||
'hours' => 'hours',
|
||||
'image' => 'image',
|
||||
'published' => 'published',
|
||||
];
|
||||
|
||||
/**
|
||||
* Process a CSV file and import locations.
|
||||
*
|
||||
* @param string $filePath Path to the uploaded CSV file.
|
||||
* @param string $delimiter CSV delimiter character.
|
||||
*
|
||||
* @return array ['imported' => int, 'skipped' => int, 'errors' => array]
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function processImport(string $filePath, string $delimiter = ','): array
|
||||
{
|
||||
$result = ['imported' => 0, 'skipped' => 0, 'errors' => []];
|
||||
|
||||
$file = new SplFileObject($filePath, 'r');
|
||||
$file->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE);
|
||||
$file->setCsvControl($delimiter);
|
||||
|
||||
// Read and map headers (strip UTF-8 BOM from Excel exports)
|
||||
$headers = $file->fgetcsv();
|
||||
|
||||
if (!empty($headers[0]))
|
||||
{
|
||||
$headers[0] = ltrim($headers[0], "\xEF\xBB\xBF");
|
||||
}
|
||||
|
||||
if (!$headers || \count($headers) < 2)
|
||||
{
|
||||
$result['errors'][] = 'Invalid CSV headers.';
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$mapping = $this->mapColumns($headers);
|
||||
|
||||
if (!isset($mapping['title']))
|
||||
{
|
||||
$result['errors'][] = 'CSV must contain a "title" or "name" column.';
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$db = $this->getDatabase();
|
||||
$table = $this->getMVCFactory()->createTable('Location', 'Administrator');
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
$rowNum = 1;
|
||||
|
||||
foreach ($file as $row)
|
||||
{
|
||||
$rowNum++;
|
||||
|
||||
if (empty($row) || (\count($row) === 1 && $row[0] === null))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $this->mapRowToData($row, $headers, $mapping);
|
||||
|
||||
if (empty($data['title']))
|
||||
{
|
||||
$result['errors'][] = "Row $rowNum: missing title";
|
||||
$result['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data['published'] = (int) ($data['published'] ?? 1);
|
||||
$data['created_by'] = $user->id;
|
||||
|
||||
// Reset table state for each row
|
||||
$table->reset();
|
||||
$table->id = 0;
|
||||
|
||||
if (!$table->bind($data))
|
||||
{
|
||||
$result['errors'][] = "Row $rowNum: " . $table->getError();
|
||||
$result['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$table->check())
|
||||
{
|
||||
$result['errors'][] = "Row $rowNum: " . $table->getError();
|
||||
$result['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$table->store())
|
||||
{
|
||||
$result['errors'][] = "Row $rowNum: " . $table->getError();
|
||||
$result['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$result['imported']++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-map CSV headers to database field names.
|
||||
*
|
||||
* @param array $headers CSV column headers.
|
||||
*
|
||||
* @return array Associative array of db_field => csv_index.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
private function mapColumns(array $headers): array
|
||||
{
|
||||
$mapping = [];
|
||||
|
||||
foreach ($headers as $index => $header)
|
||||
{
|
||||
$normalized = strtolower(trim(str_replace([' ', '-', '_'], ['_', '_', '_'], $header)));
|
||||
|
||||
if (isset(self::COLUMN_MAP[$normalized]))
|
||||
{
|
||||
$dbField = self::COLUMN_MAP[$normalized];
|
||||
|
||||
if (!isset($mapping[$dbField]))
|
||||
{
|
||||
$mapping[$dbField] = $index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a CSV row to a data array using the column mapping.
|
||||
*
|
||||
* @param array $row CSV row values.
|
||||
* @param array $headers CSV column headers.
|
||||
* @param array $mapping Column mapping (db_field => csv_index).
|
||||
*
|
||||
* @return array Data array ready for table bind.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
private function mapRowToData(array $row, array $headers, array $mapping): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($mapping as $dbField => $csvIndex)
|
||||
{
|
||||
$data[$dbField] = trim($row[$csvIndex] ?? '');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,11 @@ namespace Moko\Component\MokoSuiteStoreLocator\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Model\AdminModel;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\Form;
|
||||
use Joomla\CMS\Http\HttpFactory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Model\AdminModel;
|
||||
use Joomla\CMS\Table\Table;
|
||||
|
||||
/**
|
||||
@@ -84,4 +87,97 @@ class LocationModel extends AdminModel
|
||||
{
|
||||
return parent::getTable($name, $prefix, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the location, auto-geocoding the address if coordinates are empty.
|
||||
*
|
||||
* @param array $data The form data.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function save($data)
|
||||
{
|
||||
$hasCoords = isset($data['latitude'], $data['longitude'])
|
||||
&& is_numeric($data['latitude']) && is_numeric($data['longitude']);
|
||||
$hasAddress = !empty($data['address']) || !empty($data['city']) || !empty($data['postcode']);
|
||||
|
||||
if (!$hasCoords && $hasAddress)
|
||||
{
|
||||
$coords = $this->geocodeAddress($data);
|
||||
|
||||
if ($coords)
|
||||
{
|
||||
$data['latitude'] = $coords['lat'];
|
||||
$data['longitude'] = $coords['lng'];
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
Text::_('COM_MOKOJOOMSTORELOCATOR_GEOCODING_SUCCESS'),
|
||||
'success'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::save($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Geocode an address using the Nominatim (OpenStreetMap) API.
|
||||
*
|
||||
* @param array $data Location data with address fields.
|
||||
*
|
||||
* @return array|null ['lat' => float, 'lng' => float] or null on failure.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
private function geocodeAddress(array $data): ?array
|
||||
{
|
||||
$parts = array_filter([
|
||||
$data['address'] ?? '',
|
||||
$data['city'] ?? '',
|
||||
$data['state'] ?? '',
|
||||
$data['postcode'] ?? '',
|
||||
$data['country'] ?? '',
|
||||
]);
|
||||
|
||||
if (empty($parts))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = implode(', ', $parts);
|
||||
|
||||
try
|
||||
{
|
||||
$http = HttpFactory::getHttp();
|
||||
$url = 'https://nominatim.openstreetmap.org/search?'
|
||||
. http_build_query(['format' => 'json', 'limit' => 1, 'q' => $query]);
|
||||
$response = $http->get($url, ['User-Agent' => 'MokoSuiteStoreLocator/1.1 (+https://mokoconsulting.tech)'], 10);
|
||||
|
||||
if ($response->code !== 200)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$results = json_decode($response->body, true);
|
||||
|
||||
if (isset($results[0]['lat']) && is_numeric($results[0]['lat'])
|
||||
&& isset($results[0]['lon']) && is_numeric($results[0]['lon']))
|
||||
{
|
||||
return [
|
||||
'lat' => round((float) $results[0]['lat'], 8),
|
||||
'lng' => round((float) $results[0]['lon'], 8),
|
||||
];
|
||||
}
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
Text::sprintf('COM_MOKOJOOMSTORELOCATOR_GEOCODING_FAILED', $e->getMessage()),
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuiteStoreLocator
|
||||
* @subpackage com_mokosuitestorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\View\Import;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
/**
|
||||
* Import view for CSV upload.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* Display the import form.
|
||||
*
|
||||
* @param string $tpl The template name.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT'), 'upload');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -87,5 +87,6 @@ class HtmlView extends BaseHtmlView
|
||||
ToolbarHelper::publish('locations.publish', 'JTOOLBAR_PUBLISH', true);
|
||||
ToolbarHelper::unpublish('locations.unpublish', 'JTOOLBAR_UNPUBLISH', true);
|
||||
ToolbarHelper::deleteList('', 'locations.delete', 'JTOOLBAR_DELETE');
|
||||
ToolbarHelper::custom('import.display', 'upload', '', 'COM_MOKOJOOMSTORELOCATOR_IMPORT', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuiteStoreLocator
|
||||
* @subpackage com_mokosuitestorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
|
||||
/** @var \Moko\Component\MokoSuiteStoreLocator\Administrator\View\Import\HtmlView $this */
|
||||
?>
|
||||
<form action="<?php echo Route::_('index.php?option=com_mokosuitestorelocator&task=import.import'); ?>"
|
||||
method="post" enctype="multipart/form-data" class="form-validate">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_UPLOAD'); ?></h3>
|
||||
<p class="text-muted"><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_FILE_DESC'); ?></p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="csv_file" class="form-label">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_FILE'); ?>
|
||||
</label>
|
||||
<input type="file" name="jform[csv_file]" id="csv_file"
|
||||
class="form-control" accept=".csv,text/csv" required />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="delimiter" class="form-label">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_DELIMITER'); ?>
|
||||
</label>
|
||||
<select name="delimiter" id="delimiter" class="form-select" style="width: auto;">
|
||||
<option value=","><?php echo Text::_('Comma (,)'); ?></option>
|
||||
<option value=";"><?php echo Text::_('Semicolon (;)'); ?></option>
|
||||
<option value="|"><?php echo Text::_('Pipe (|)'); ?></option>
|
||||
<option value="	"><?php echo Text::_('Tab'); ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="icon-upload" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT'); ?>
|
||||
</button>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitestorelocator&view=locations'); ?>"
|
||||
class="btn btn-secondary ms-2">
|
||||
<?php echo Text::_('JCANCEL'); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4><?php echo Text::_('JHELP'); ?></h4>
|
||||
<p>Supported column headers (auto-detected):</p>
|
||||
<ul class="small">
|
||||
<li><strong>title</strong> / name / store / location (required)</li>
|
||||
<li>address / street</li>
|
||||
<li>city</li>
|
||||
<li>state / province / region</li>
|
||||
<li>postcode / zip / zipcode / postal_code</li>
|
||||
<li>country</li>
|
||||
<li>latitude / lat</li>
|
||||
<li>longitude / lng / lon</li>
|
||||
<li>phone / telephone</li>
|
||||
<li>email</li>
|
||||
<li>website / url</li>
|
||||
<li>hours</li>
|
||||
<li>published (0 or 1)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php echo HTMLHelper::_('form.token'); ?>
|
||||
</form>
|
||||
@@ -57,6 +57,7 @@
|
||||
<menu>COM_MOKOJOOMSTORELOCATOR</menu>
|
||||
<submenu>
|
||||
<menu link="option=com_mokosuitestorelocator&view=locations">COM_MOKOJOOMSTORELOCATOR_LOCATIONS</menu>
|
||||
<menu link="option=com_mokosuitestorelocator&view=import">COM_MOKOJOOMSTORELOCATOR_IMPORT</menu>
|
||||
</submenu>
|
||||
</administration>
|
||||
</extension>
|
||||
|
||||
+1
@@ -17,3 +17,4 @@ COM_MOKOJOOMSTORELOCATOR_LOCATIONS_VIEW_DEFAULT_DESC="Displays a list of all sto
|
||||
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"
|
||||
COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS="Get Directions"
|
||||
|
||||
@@ -67,6 +67,38 @@ class LocationsModel extends ListModel
|
||||
$state = $app->input->getString('state', '');
|
||||
$this->setState('filter.state', $state);
|
||||
|
||||
$latRaw = $app->input->getString('lat', '');
|
||||
$lngRaw = $app->input->getString('lng', '');
|
||||
$radius = $app->input->getInt('radius', 0);
|
||||
$radiusUnit = $app->input->getString('radius_unit', 'miles');
|
||||
|
||||
if ($latRaw !== '' && is_numeric($latRaw))
|
||||
{
|
||||
$lat = (float) $latRaw;
|
||||
|
||||
if ($lat >= -90 && $lat <= 90)
|
||||
{
|
||||
$this->setState('filter.lat', $lat);
|
||||
}
|
||||
}
|
||||
|
||||
if ($lngRaw !== '' && is_numeric($lngRaw))
|
||||
{
|
||||
$lng = (float) $lngRaw;
|
||||
|
||||
if ($lng >= -180 && $lng <= 180)
|
||||
{
|
||||
$this->setState('filter.lng', $lng);
|
||||
}
|
||||
}
|
||||
|
||||
if ($radius > 0 && $radius <= 25000)
|
||||
{
|
||||
$this->setState('filter.radius', $radius);
|
||||
}
|
||||
|
||||
$this->setState('filter.radius_unit', in_array($radiusUnit, ['miles', 'km']) ? $radiusUnit : 'miles');
|
||||
|
||||
parent::populateState($ordering, $direction);
|
||||
}
|
||||
|
||||
@@ -122,10 +154,36 @@ class LocationsModel extends ListModel
|
||||
->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));
|
||||
// Proximity / Haversine distance filter
|
||||
$lat = $this->getState('filter.lat');
|
||||
$lng = $this->getState('filter.lng');
|
||||
$radius = $this->getState('filter.radius');
|
||||
|
||||
if ($lat !== null && $lng !== null && $radius)
|
||||
{
|
||||
$unit = $this->getState('filter.radius_unit', 'miles');
|
||||
$earthRadius = ($unit === 'km') ? 6371 : 3959;
|
||||
|
||||
$haversine = '(' . $earthRadius . ' * ACOS(LEAST(1, GREATEST(-1, '
|
||||
. 'SIN(RADIANS(' . $db->quoteName('a.latitude') . ')) * SIN(RADIANS(' . (float) $lat . ')) '
|
||||
. '+ COS(RADIANS(' . $db->quoteName('a.latitude') . ')) * COS(RADIANS(' . (float) $lat . ')) '
|
||||
. '* COS(RADIANS(' . $db->quoteName('a.longitude') . ' - ' . (float) $lng . '))'
|
||||
. '))))';
|
||||
|
||||
$query->where($db->quoteName('a.latitude') . ' IS NOT NULL')
|
||||
->where($db->quoteName('a.longitude') . ' IS NOT NULL')
|
||||
->where($haversine . ' <= ' . (int) $radius);
|
||||
|
||||
$query->select($haversine . ' AS distance');
|
||||
$query->order('distance ASC');
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default 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;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,20 @@ $item = $this->item;
|
||||
<?php if ($item->country) : ?>
|
||||
<br><span itemprop="addressCountry"><?php echo $this->escape($item->country); ?></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->latitude && $item->longitude) : ?>
|
||||
<div class="com-mokosuitestorelocator-location__directions mt-2">
|
||||
<a href="https://www.google.com/maps/dir/?api=1&destination=<?php echo (float) $item->latitude; ?>,<?php echo (float) $item->longitude; ?>"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
target="_blank" rel="noopener"
|
||||
data-directions
|
||||
data-lat="<?php echo (float) $item->latitude; ?>"
|
||||
data-lng="<?php echo (float) $item->longitude; ?>"
|
||||
data-title="<?php echo $this->escape($item->title); ?>">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="com-mokosuitestorelocator-location__contact">
|
||||
@@ -57,7 +71,8 @@ $item = $this->item;
|
||||
<?php if ($item->phone) : ?>
|
||||
<div>
|
||||
<strong><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_FIELD_PHONE'); ?>:</strong>
|
||||
<a href="tel:<?php echo $this->escape($item->phone); ?>" itemprop="telephone">
|
||||
<?php $safePhone = preg_replace('/[^0-9+\-() ]/', '', $item->phone); ?>
|
||||
<a href="tel:<?php echo $this->escape($safePhone); ?>" itemprop="telephone">
|
||||
<?php echo $this->escape($item->phone); ?>
|
||||
</a>
|
||||
</div>
|
||||
@@ -72,7 +87,7 @@ $item = $this->item;
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->website) : ?>
|
||||
<?php if ($item->website && preg_match('#^https?://#i', $item->website)) : ?>
|
||||
<div>
|
||||
<strong><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_FIELD_WEBSITE'); ?>:</strong>
|
||||
<a href="<?php echo $this->escape($item->website); ?>" itemprop="url" target="_blank" rel="noopener">
|
||||
|
||||
@@ -54,7 +54,8 @@ use Joomla\CMS\Router\Route;
|
||||
|
||||
<?php if ($item->phone) : ?>
|
||||
<div class="com-mokosuitestorelocator-location-card__phone">
|
||||
<a href="tel:<?php echo $this->escape($item->phone); ?>" itemprop="telephone">
|
||||
<?php $safePhone = preg_replace('/[^0-9+\-() ]/', '', $item->phone); ?>
|
||||
<a href="tel:<?php echo $this->escape($safePhone); ?>" itemprop="telephone">
|
||||
<?php echo $this->escape($item->phone); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
var popup = '<strong>' + esc(loc.title) + '</strong>';
|
||||
if (loc.address) popup += '<br>' + esc(loc.address);
|
||||
if (loc.phone) popup += '<br><a href="tel:' + esc(loc.phone) + '">' + esc(loc.phone) + '</a>';
|
||||
popup += '<br><a href="https://www.google.com/maps/dir/?api=1&destination=' + loc.lat + ',' + loc.lng + '" target="_blank" rel="noopener"><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS', true); ?></a>';
|
||||
marker.bindPopup(popup);
|
||||
bounds.extend([loc.lat, loc.lng]);
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@ $moduleId = $displayData['module']->id;
|
||||
|
||||
<input type="hidden" name="lat" id="mokosuitestorelocator-lat-<?php echo (int) $moduleId; ?>" value="" />
|
||||
<input type="hidden" name="lng" id="mokosuitestorelocator-lng-<?php echo (int) $moduleId; ?>" value="" />
|
||||
<input type="hidden" name="radius_unit" value="<?php echo $this->escape($radiusUnit); ?>" />
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary mokosuitestorelocator-geolocation-btn"
|
||||
id="mokosuitestorelocator-geolocate-<?php echo (int) $moduleId; ?>">
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Location edit form -->
|
||||
<form>
|
||||
<fieldset name="details" addfieldprefix="Moko\Component\MokoJoomStoreLocator\Administrator\Field">
|
||||
<field
|
||||
name="id"
|
||||
type="hidden"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="title"
|
||||
type="text"
|
||||
label="JGLOBAL_TITLE"
|
||||
required="true"
|
||||
size="40"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="alias"
|
||||
type="text"
|
||||
label="JFIELD_ALIAS_LABEL"
|
||||
size="40"
|
||||
hint="JFIELD_ALIAS_PLACEHOLDER"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="description"
|
||||
type="editor"
|
||||
label="JGLOBAL_DESCRIPTION"
|
||||
filter="safehtml"
|
||||
buttons="true"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="published"
|
||||
type="list"
|
||||
label="JSTATUS"
|
||||
default="1"
|
||||
>
|
||||
<option value="1">JPUBLISHED</option>
|
||||
<option value="0">JUNPUBLISHED</option>
|
||||
<option value="-2">JTRASHED</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="address" label="COM_MOKOJOOMSTORELOCATOR_FIELDSET_ADDRESS">
|
||||
<field
|
||||
name="address"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_ADDRESS"
|
||||
size="60"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="city"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_CITY"
|
||||
size="40"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="state"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_STATE"
|
||||
size="40"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="postcode"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_POSTCODE"
|
||||
size="20"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="country"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_COUNTRY"
|
||||
size="40"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="coordinates" label="COM_MOKOJOOMSTORELOCATOR_FIELDSET_COORDINATES">
|
||||
<field
|
||||
name="latitude"
|
||||
type="number"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_LATITUDE"
|
||||
step="0.00000001"
|
||||
min="-90"
|
||||
max="90"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="longitude"
|
||||
type="number"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_LONGITUDE"
|
||||
step="0.00000001"
|
||||
min="-180"
|
||||
max="180"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="contact" label="COM_MOKOJOOMSTORELOCATOR_FIELDSET_CONTACT">
|
||||
<field
|
||||
name="phone"
|
||||
type="tel"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_PHONE"
|
||||
size="30"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="email"
|
||||
type="email"
|
||||
label="JGLOBAL_EMAIL"
|
||||
size="40"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="website"
|
||||
type="url"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_WEBSITE"
|
||||
size="60"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="hours"
|
||||
type="textarea"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_HOURS"
|
||||
rows="5"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="image" label="COM_MOKOJOOMSTORELOCATOR_FIELDSET_IMAGE">
|
||||
<field
|
||||
name="image"
|
||||
type="media"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_IMAGE"
|
||||
/>
|
||||
</fieldset>
|
||||
</form>
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
; MokoJoomStoreLocator - 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"
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
; MokoJoomStoreLocator - 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"
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage com_mokojoomstorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
|
||||
use Joomla\CMS\Extension\ComponentInterface;
|
||||
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
|
||||
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Moko\Component\MokoJoomStoreLocator\Administrator\Extension\MokoJoomStoreLocatorComponent;
|
||||
|
||||
/**
|
||||
* The store locator service provider.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
return new class implements ServiceProviderInterface
|
||||
{
|
||||
/**
|
||||
* Registers the service provider with a DI container.
|
||||
*
|
||||
* @param Container $container The DI container.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoJoomStoreLocator'));
|
||||
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoJoomStoreLocator'));
|
||||
|
||||
$container->set(
|
||||
ComponentInterface::class,
|
||||
function (Container $container) {
|
||||
$component = new MokoJoomStoreLocatorComponent(
|
||||
$container->get(ComponentDispatcherFactoryInterface::class)
|
||||
);
|
||||
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
|
||||
|
||||
return $component;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
-- =========================================================================
|
||||
-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
--
|
||||
-- MokoJoomStoreLocator - Store locations table
|
||||
-- =========================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokojoomstorelocator_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;
|
||||
@@ -1,6 +0,0 @@
|
||||
-- =========================================================================
|
||||
-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- =========================================================================
|
||||
|
||||
DROP TABLE IF EXISTS `#__mokojoomstorelocator_locations`;
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage com_mokojoomstorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoJoomStoreLocator\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
|
||||
/**
|
||||
* Default controller for the admin side of the component.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class DisplayController extends BaseController
|
||||
{
|
||||
/**
|
||||
* The default view.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $default_view = 'locations';
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage com_mokojoomstorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoJoomStoreLocator\Administrator\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\MVCComponent;
|
||||
|
||||
/**
|
||||
* Component class for com_mokojoomstorelocator.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class MokoJoomStoreLocatorComponent extends MVCComponent
|
||||
{
|
||||
// TODO: Add boot(), getRouterRules(), or custom services as needed
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage com_mokojoomstorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoJoomStoreLocator\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Model\AdminModel;
|
||||
use Joomla\CMS\Form\Form;
|
||||
use Joomla\CMS\Table\Table;
|
||||
|
||||
/**
|
||||
* Single location edit model.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class LocationModel extends AdminModel
|
||||
{
|
||||
/**
|
||||
* The type alias for this content type.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public $typeAlias = 'com_mokojoomstorelocator.location';
|
||||
|
||||
/**
|
||||
* Get the form for this model.
|
||||
*
|
||||
* @param array $data Data for the form.
|
||||
* @param boolean $loadData True if the form is to load its own data.
|
||||
*
|
||||
* @return Form|boolean A Form object on success, false on failure.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getForm($data = [], $loadData = true)
|
||||
{
|
||||
$form = $this->loadForm(
|
||||
'com_mokojoomstorelocator.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);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage com_mokojoomstorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoJoomStoreLocator\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Model\ListModel;
|
||||
use Joomla\Database\QueryInterface;
|
||||
|
||||
/**
|
||||
* Locations list model for the admin view.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class LocationsModel extends ListModel
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct($config = [])
|
||||
{
|
||||
if (empty($config['filter_fields']))
|
||||
{
|
||||
$config['filter_fields'] = [
|
||||
'id', 'a.id',
|
||||
'title', 'a.title',
|
||||
'city', 'a.city',
|
||||
'state', 'a.state',
|
||||
'published', 'a.published',
|
||||
'ordering', 'a.ordering',
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('#__mokojoomstorelocator_locations', 'a'));
|
||||
|
||||
// TODO: Add filter by published state
|
||||
// TODO: Add filter by category
|
||||
// TODO: Add search filter
|
||||
// TODO: Add ordering clause
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage com_mokojoomstorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoJoomStoreLocator\Administrator\Table;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\Database\DatabaseDriver;
|
||||
|
||||
/**
|
||||
* Location table class.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class LocationTable extends Table
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DatabaseDriver $db Database driver object.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(DatabaseDriver $db)
|
||||
{
|
||||
parent::__construct('#__mokojoomstorelocator_locations', 'id', $db);
|
||||
|
||||
$this->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
|
||||
{
|
||||
// TODO: Validate title is not empty
|
||||
// TODO: Auto-generate alias from title if empty
|
||||
// TODO: Validate latitude/longitude ranges
|
||||
// TODO: Set created/modified timestamps
|
||||
|
||||
return parent::check();
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage com_mokojoomstorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoJoomStoreLocator\Administrator\View\Locations;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
/**
|
||||
* Locations list view for the admin.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* The list of locations.
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* The pagination object.
|
||||
*
|
||||
* @var \Joomla\CMS\Pagination\Pagination
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $pagination;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* Display the view.
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
$this->items = $this->get('Items');
|
||||
$this->pagination = $this->get('Pagination');
|
||||
$this->state = $this->get('State');
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the page title and toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title('Store Locator: Locations');
|
||||
ToolbarHelper::addNew('location.add');
|
||||
ToolbarHelper::publish('locations.publish', 'JTOOLBAR_PUBLISH', true);
|
||||
ToolbarHelper::unpublish('locations.unpublish', 'JTOOLBAR_UNPUBLISH', true);
|
||||
ToolbarHelper::deleteList('', 'locations.delete', 'JTOOLBAR_DELETE');
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage com_mokojoomstorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Layout\LayoutHelper;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
/** @var \Moko\Component\MokoJoomStoreLocator\Administrator\View\Locations\HtmlView $this */
|
||||
?>
|
||||
<form action="<?php echo Route::_('index.php?option=com_mokojoomstorelocator&view=locations'); ?>"
|
||||
method="post" name="adminForm" id="adminForm">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="j-main-container" class="j-main-container">
|
||||
<?php if (empty($this->items)) : ?>
|
||||
<div class="alert alert-info">
|
||||
<span class="icon-info-circle" aria-hidden="true"></span>
|
||||
<?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<table class="table" id="locationList">
|
||||
<caption class="visually-hidden">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_TABLE_CAPTION'); ?>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="w-1 text-center">
|
||||
<?php echo HTMLHelper::_('grid.checkall'); ?>
|
||||
</td>
|
||||
<th scope="col">
|
||||
<?php echo Text::_('JGLOBAL_TITLE'); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-10 d-none d-md-table-cell">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_CITY'); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-10 d-none d-md-table-cell">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_STATE'); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-5 text-center">
|
||||
<?php echo Text::_('JSTATUS'); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-5 text-center">
|
||||
<?php echo Text::_('JGRID_HEADING_ID'); ?>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- TODO: Render location rows -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php echo $this->pagination->getListFooter(); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<input type="hidden" name="task" value="">
|
||||
<input type="hidden" name="boxchecked" value="0">
|
||||
<?php echo HTMLHelper::_('form.token'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- =========================================================================
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
=========================================================================
|
||||
FILE INFORMATION
|
||||
DEFGROUP: MokoJoomStoreLocator
|
||||
INGROUP: com_mokojoomstorelocator
|
||||
PATH: src/packages/com_mokojoomstorelocator/mokojoomstorelocator.xml
|
||||
VERSION: 01.00.00
|
||||
BRIEF: Component manifest for the store locator component
|
||||
=========================================================================
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>com_mokojoomstorelocator</name>
|
||||
<version>1.0.0</version>
|
||||
<creationDate>2026-05-21</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<description>COM_MOKOJOOMSTORELOCATOR_DESC</description>
|
||||
|
||||
<namespace path="src">Moko\Component\MokoJoomStoreLocator</namespace>
|
||||
|
||||
<install>
|
||||
<sql>
|
||||
<file driver="mysql" charset="utf8">sql/install.mysql.sql</file>
|
||||
</sql>
|
||||
</install>
|
||||
|
||||
<uninstall>
|
||||
<sql>
|
||||
<file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file>
|
||||
</sql>
|
||||
</uninstall>
|
||||
|
||||
<files folder="site">
|
||||
<folder>language</folder>
|
||||
<folder>src</folder>
|
||||
<folder>tmpl</folder>
|
||||
</files>
|
||||
|
||||
<administration>
|
||||
<files folder="admin">
|
||||
<folder>forms</folder>
|
||||
<folder>language</folder>
|
||||
<folder>services</folder>
|
||||
<folder>sql</folder>
|
||||
<folder>src</folder>
|
||||
<folder>tmpl</folder>
|
||||
</files>
|
||||
|
||||
<menu>COM_MOKOJOOMSTORELOCATOR</menu>
|
||||
<submenu>
|
||||
<menu link="option=com_mokojoomstorelocator&view=locations">COM_MOKOJOOMSTORELOCATOR_LOCATIONS</menu>
|
||||
</submenu>
|
||||
</administration>
|
||||
</extension>
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
; MokoJoomStoreLocator 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."
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
; MokoJoomStoreLocator 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."
|
||||
@@ -1,72 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- =========================================================================
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
=========================================================================
|
||||
FILE INFORMATION
|
||||
DEFGROUP: MokoJoomStoreLocator
|
||||
INGROUP: mod_mokojoomstorelocator_map
|
||||
PATH: src/packages/mod_mokojoomstorelocator_map/mod_mokojoomstorelocator_map.xml
|
||||
VERSION: 01.00.00
|
||||
BRIEF: Module manifest for the store locator map module
|
||||
=========================================================================
|
||||
-->
|
||||
<extension type="module" client="site" method="upgrade">
|
||||
<name>mod_mokojoomstorelocator_map</name>
|
||||
<version>1.0.0</version>
|
||||
<creationDate>2026-05-21</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<description>MOD_MOKOJOOMSTORELOCATOR_MAP_DESC</description>
|
||||
|
||||
<namespace path="src">Moko\Module\MokoJoomStoreLocatorMap</namespace>
|
||||
|
||||
<files>
|
||||
<folder>src</folder>
|
||||
<folder>tmpl</folder>
|
||||
<folder>language</folder>
|
||||
</files>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic">
|
||||
<field
|
||||
name="map_height"
|
||||
type="text"
|
||||
label="MOD_MOKOJOOMSTORELOCATOR_MAP_HEIGHT"
|
||||
default="400px"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="map_zoom"
|
||||
type="number"
|
||||
label="MOD_MOKOJOOMSTORELOCATOR_MAP_ZOOM"
|
||||
default="10"
|
||||
min="1"
|
||||
max="20"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="map_provider"
|
||||
type="list"
|
||||
label="MOD_MOKOJOOMSTORELOCATOR_MAP_PROVIDER"
|
||||
default="leaflet"
|
||||
>
|
||||
<option value="leaflet">OpenStreetMap (Leaflet)</option>
|
||||
<option value="google">Google Maps</option>
|
||||
</field>
|
||||
|
||||
<field
|
||||
name="api_key"
|
||||
type="text"
|
||||
label="MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY"
|
||||
description="MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY_DESC"
|
||||
/>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage mod_mokojoomstorelocator_map
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Module\MokoJoomStoreLocatorMap\Dispatcher;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
|
||||
use Joomla\CMS\Helper\HelperFactoryAwareInterface;
|
||||
use Joomla\CMS\Helper\HelperFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* Dispatcher for mod_mokojoomstorelocator_map.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareInterface
|
||||
{
|
||||
use HelperFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* Returns the layout data.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected function getLayoutData(): array
|
||||
{
|
||||
$data = parent::getLayoutData();
|
||||
|
||||
// TODO: Load published locations with coordinates from the component table
|
||||
// TODO: Build marker data array for the map JS
|
||||
$data['locations'] = [];
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage mod_mokojoomstorelocator_map
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Language\Text;
|
||||
|
||||
/** @var array $displayData */
|
||||
$params = $displayData['params'];
|
||||
$locations = $displayData['locations'] ?? [];
|
||||
$mapHeight = $params->get('map_height', '400px');
|
||||
$mapZoom = (int) $params->get('map_zoom', 10);
|
||||
$provider = $params->get('map_provider', 'leaflet');
|
||||
?>
|
||||
<div class="mod-mokojoomstorelocator-map"
|
||||
id="mokojoomstorelocator-map-<?php echo $displayData['module']->id; ?>"
|
||||
style="height: <?php echo $this->escape($mapHeight); ?>;"
|
||||
data-locations="<?php echo $this->escape(json_encode($locations)); ?>"
|
||||
data-zoom="<?php echo $mapZoom; ?>"
|
||||
data-provider="<?php echo $this->escape($provider); ?>">
|
||||
<!-- TODO: Map renders here via JavaScript -->
|
||||
<noscript>
|
||||
<p><?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_MAP_NOSCRIPT'); ?></p>
|
||||
</noscript>
|
||||
</div>
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
; MokoJoomStoreLocator 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)"
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
; MokoJoomStoreLocator 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."
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- =========================================================================
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
=========================================================================
|
||||
FILE INFORMATION
|
||||
DEFGROUP: MokoJoomStoreLocator
|
||||
INGROUP: mod_mokojoomstorelocator_search
|
||||
PATH: src/packages/mod_mokojoomstorelocator_search/mod_mokojoomstorelocator_search.xml
|
||||
VERSION: 01.00.00
|
||||
BRIEF: Module manifest for the store locator search module
|
||||
=========================================================================
|
||||
-->
|
||||
<extension type="module" client="site" method="upgrade">
|
||||
<name>mod_mokojoomstorelocator_search</name>
|
||||
<version>1.0.0</version>
|
||||
<creationDate>2026-05-21</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<description>MOD_MOKOJOOMSTORELOCATOR_SEARCH_DESC</description>
|
||||
|
||||
<namespace path="src">Moko\Module\MokoJoomStoreLocatorSearch</namespace>
|
||||
|
||||
<files>
|
||||
<folder>src</folder>
|
||||
<folder>tmpl</folder>
|
||||
<folder>language</folder>
|
||||
</files>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic">
|
||||
<field
|
||||
name="show_city_filter"
|
||||
type="radio"
|
||||
label="MOD_MOKOJOOMSTORELOCATOR_SEARCH_SHOW_CITY"
|
||||
default="1"
|
||||
class="btn-group"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<field
|
||||
name="show_radius_filter"
|
||||
type="radio"
|
||||
label="MOD_MOKOJOOMSTORELOCATOR_SEARCH_SHOW_RADIUS"
|
||||
default="1"
|
||||
class="btn-group"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<field
|
||||
name="radius_unit"
|
||||
type="list"
|
||||
label="MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS_UNIT"
|
||||
default="miles"
|
||||
>
|
||||
<option value="miles">Miles</option>
|
||||
<option value="km">Kilometres</option>
|
||||
</field>
|
||||
|
||||
<field
|
||||
name="radius_options"
|
||||
type="text"
|
||||
label="MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS_OPTIONS"
|
||||
default="5,10,25,50,100"
|
||||
description="MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS_OPTIONS_DESC"
|
||||
/>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage mod_mokojoomstorelocator_search
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Module\MokoJoomStoreLocatorSearch\Dispatcher;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
|
||||
use Joomla\CMS\Helper\HelperFactoryAwareInterface;
|
||||
use Joomla\CMS\Helper\HelperFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* Dispatcher for mod_mokojoomstorelocator_search.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareInterface
|
||||
{
|
||||
use HelperFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* Returns the layout data.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected function getLayoutData(): array
|
||||
{
|
||||
$data = parent::getLayoutData();
|
||||
|
||||
// TODO: Load distinct cities/states for filter dropdowns
|
||||
// TODO: Build radius options array from params
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage mod_mokojoomstorelocator_search
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
/** @var array $displayData */
|
||||
$params = $displayData['params'];
|
||||
?>
|
||||
<div class="mod-mokojoomstorelocator-search">
|
||||
<form action="<?php echo Route::_('index.php?option=com_mokojoomstorelocator&view=locations'); ?>"
|
||||
method="get" class="mokojoomstorelocator-search-form">
|
||||
|
||||
<div class="mokojoomstorelocator-search-field">
|
||||
<label for="mokojoomstorelocator-query">
|
||||
<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_LABEL'); ?>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="mokojoomstorelocator-query"
|
||||
name="filter_search"
|
||||
placeholder="<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_PLACEHOLDER'); ?>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- TODO: City dropdown filter -->
|
||||
<!-- TODO: Radius dropdown filter -->
|
||||
<!-- TODO: Geolocation "Use my location" button -->
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<?php echo Text::_('JSEARCH_FILTER_SUBMIT'); ?>
|
||||
</button>
|
||||
|
||||
<input type="hidden" name="option" value="com_mokojoomstorelocator" />
|
||||
<input type="hidden" name="view" value="locations" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- =========================================================================
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
=========================================================================
|
||||
FILE INFORMATION
|
||||
DEFGROUP: MokoJoomStoreLocator
|
||||
INGROUP: pkg_mokojoomstorelocator
|
||||
PATH: src/pkg_mokojoomstorelocator.xml
|
||||
VERSION: 01.00.00
|
||||
BRIEF: Package manifest for the MokoJoomStoreLocator package
|
||||
=========================================================================
|
||||
-->
|
||||
<extension type="package" method="upgrade">
|
||||
<name>pkg_mokojoomstorelocator</name>
|
||||
<packagename>mokojoomstorelocator</packagename>
|
||||
<version>1.0.0</version>
|
||||
<creationDate>2026-05-21</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<description>PKG_MOKOJOOMSTORELOCATOR_DESC</description>
|
||||
<scriptfile>script.php</scriptfile>
|
||||
|
||||
<files>
|
||||
<file type="component" id="com_mokojoomstorelocator">com_mokojoomstorelocator.zip</file>
|
||||
<file type="module" id="mod_mokojoomstorelocator_map" client="site">mod_mokojoomstorelocator_map.zip</file>
|
||||
<file type="module" id="mod_mokojoomstorelocator_search" client="site">mod_mokojoomstorelocator_search.zip</file>
|
||||
</files>
|
||||
|
||||
<updateservers>
|
||||
<server type="extension" name="MokoJoomStoreLocator Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomStoreLocator/updates.xml</server>
|
||||
</updateservers>
|
||||
<dlid prefix="dlid=" suffix=""/>
|
||||
<blockChildUninstall>true</blockChildUninstall>
|
||||
</extension>
|
||||
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoJoomStoreLocator
|
||||
* @subpackage pkg_mokojoomstorelocator
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Installer\InstallerAdapter;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
/**
|
||||
* Package installation script for MokoJoomStoreLocator.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Pkg_MokojoomstorelocatorInstallerScript
|
||||
{
|
||||
/**
|
||||
* Minimum PHP version required.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $minimumPhp = '8.1';
|
||||
|
||||
/**
|
||||
* Minimum Joomla version required.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $minimumJoomla = '4.4.0';
|
||||
|
||||
/**
|
||||
* Called before any type of action.
|
||||
*
|
||||
* @param string $type Installation type (install, update, discover_install).
|
||||
* @param InstallerAdapter $parent The parent installer object.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function preflight($type, $parent)
|
||||
{
|
||||
if (version_compare(PHP_VERSION, $this->minimumPhp, '<'))
|
||||
{
|
||||
Log::add(
|
||||
'MokoJoomStoreLocator requires PHP ' . $this->minimumPhp . ' or later.',
|
||||
Log::WARNING,
|
||||
'jerror'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version_compare(JVERSION, $this->minimumJoomla, '<'))
|
||||
{
|
||||
Log::add(
|
||||
'MokoJoomStoreLocator requires Joomla ' . $this->minimumJoomla . ' or later.',
|
||||
Log::WARNING,
|
||||
'jerror'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after installation.
|
||||
*
|
||||
* @param string $type Installation type.
|
||||
* @param InstallerAdapter $parent The parent installer object.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function postflight($type, $parent)
|
||||
{
|
||||
// TODO: Post-installation tasks (enable modules, set defaults, etc.)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on uninstallation.
|
||||
*
|
||||
* @param InstallerAdapter $parent The parent installer object.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function uninstall($parent)
|
||||
{
|
||||
// TODO: Cleanup tasks on uninstall
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user