feat: core store locator implementation (dev release) #28
@@ -114,6 +114,7 @@ build/
|
||||
dist/
|
||||
out/
|
||||
site/
|
||||
!src/**/site/
|
||||
*.map
|
||||
*.css.map
|
||||
*.js.map
|
||||
|
||||
+33
@@ -28,3 +28,36 @@ COM_MOKOJOOMSTORELOCATOR_FIELD_PHONE="Phone"
|
||||
COM_MOKOJOOMSTORELOCATOR_FIELD_WEBSITE="Website"
|
||||
COM_MOKOJOOMSTORELOCATOR_FIELD_HOURS="Business Hours"
|
||||
COM_MOKOJOOMSTORELOCATOR_FIELD_IMAGE="Location Image"
|
||||
|
||||
COM_MOKOJOOMSTORELOCATOR_FIELDSET_GEOCODING="Geocoding"
|
||||
COM_MOKOJOOMSTORELOCATOR_FIELD_GEOCODER_PROVIDER="Geocoding Provider"
|
||||
COM_MOKOJOOMSTORELOCATOR_FIELD_GOOGLE_API_KEY="Google API Key"
|
||||
COM_MOKOJOOMSTORELOCATOR_FIELD_GOOGLE_API_KEY_DESC="Required for Google Geocoding and Google Maps. Get one at console.cloud.google.com"
|
||||
COM_MOKOJOOMSTORELOCATOR_FIELD_AUTO_GEOCODE="Auto-Geocode on Save"
|
||||
COM_MOKOJOOMSTORELOCATOR_FIELD_AUTO_GEOCODE_DESC="Automatically convert addresses to coordinates when saving a location."
|
||||
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT="Import"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_TITLE="Import Locations from CSV"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FILE="CSV File"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FILE_DESC="Upload a CSV file with location data. First row must be column headers."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_NO_FILE="No file was uploaded."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_INVALID_FORMAT="Invalid file format. Only CSV files are accepted."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_GEOCODE="Geocode missing coordinates"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_GEOCODE_DESC="Auto-fill latitude/longitude for locations without coordinates. Uses your configured geocoding provider."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_UPDATE_EXISTING="Update existing locations"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_UPDATE_EXISTING_DESC="Match by title and update existing records instead of creating duplicates."
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_SUBMIT="Import Locations"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FORMAT="CSV Format"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FORMAT_DESC="The CSV file must have a header row. Supported columns:"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_DOWNLOAD_TEMPLATE="Download CSV Template"
|
||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_RESULT="Import complete: %d imported, %d updated, %d skipped."
|
||||
|
||||
COM_MOKOJOOMSTORELOCATOR_EXPORT="Export"
|
||||
COM_MOKOJOOMSTORELOCATOR_EXPORT_CSV="Export to CSV"
|
||||
|
||||
COM_MOKOJOOMSTORELOCATOR_SAMPLEDATA="Sample Data"
|
||||
COM_MOKOJOOMSTORELOCATOR_SAMPLEDATA_INJECT="Install Sample Data"
|
||||
COM_MOKOJOOMSTORELOCATOR_SAMPLEDATA_INJECT_CONFIRM="This will add 8 sample store locations to your database. Continue?"
|
||||
COM_MOKOJOOMSTORELOCATOR_SAMPLEDATA_INJECTED="%d sample locations installed successfully."
|
||||
|
||||
COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS="Get Directions"
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\Session\Session;
|
||||
|
||||
/**
|
||||
* Controller for exporting locations to CSV.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class ExportController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Export locations as a CSV download.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function execute(): void
|
||||
{
|
||||
Session::checkToken('get') or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
$query->select('a.*')
|
||||
->from($db->quoteName('#__mokojoomstorelocator_locations', 'a'))
|
||||
->order($db->quoteName('a.title') . ' ASC');
|
||||
|
||||
// Apply filters from request
|
||||
$app = Factory::getApplication();
|
||||
$published = $app->getInput()->getInt('filter_published', null);
|
||||
|
||||
if ($published !== null)
|
||||
{
|
||||
$query->where($db->quoteName('a.published') . ' = :published')
|
||||
->bind(':published', $published, \Joomla\Database\ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
$city = $app->getInput()->getString('filter_city', '');
|
||||
|
||||
if ($city)
|
||||
{
|
||||
$query->where($db->quoteName('a.city') . ' = :city')
|
||||
->bind(':city', $city);
|
||||
}
|
||||
|
||||
$db->setQuery($query);
|
||||
$locations = $db->loadObjectList();
|
||||
|
||||
// CSV columns
|
||||
$columns = [
|
||||
'id', 'title', 'alias', 'description', 'address', 'city', 'state',
|
||||
'postcode', 'country', 'latitude', 'longitude', 'phone', 'email',
|
||||
'website', 'hours', 'published',
|
||||
];
|
||||
|
||||
// Output CSV
|
||||
$app = Factory::getApplication();
|
||||
$app->setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
$app->setHeader('Content-Disposition', 'attachment; filename="store-locations-' . date('Y-m-d') . '.csv"');
|
||||
$app->setHeader('Cache-Control', 'no-cache, must-revalidate');
|
||||
$app->sendHeaders();
|
||||
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
// BOM for Excel UTF-8 compatibility
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
|
||||
// Header row
|
||||
fputcsv($output, $columns);
|
||||
|
||||
// Data rows
|
||||
foreach ($locations as $location)
|
||||
{
|
||||
$row = [];
|
||||
|
||||
foreach ($columns as $col)
|
||||
{
|
||||
$row[] = $location->$col ?? '';
|
||||
}
|
||||
|
||||
fputcsv($output, $row);
|
||||
}
|
||||
|
||||
fclose($output);
|
||||
|
||||
$app->close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?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\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
use Moko\Component\MokoJoomStoreLocator\Administrator\Helper\Geocoder;
|
||||
|
||||
/**
|
||||
* Controller for importing locations from CSV files.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class ImportController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Process the uploaded CSV file and import locations.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function execute(): void
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
$app = Factory::getApplication();
|
||||
$input = $app->getInput();
|
||||
$file = $input->files->get('import_file', [], 'array');
|
||||
|
||||
if (empty($file['tmp_name']) || $file['error'] !== UPLOAD_ERR_OK)
|
||||
{
|
||||
$app->enqueueMessage(Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_NO_FILE'), 'error');
|
||||
$app->redirect(Route::_('index.php?option=com_mokojoomstorelocator&view=import', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
|
||||
if ($ext !== 'csv')
|
||||
{
|
||||
$app->enqueueMessage(Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_INVALID_FORMAT'), 'error');
|
||||
$app->redirect(Route::_('index.php?option=com_mokojoomstorelocator&view=import', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$geocodeOnImport = (bool) $input->getInt('geocode', 0);
|
||||
$updateExisting = (bool) $input->getInt('update_existing', 0);
|
||||
|
||||
$result = $this->processCSV($file['tmp_name'], $geocodeOnImport, $updateExisting);
|
||||
|
||||
$app->enqueueMessage(
|
||||
Text::sprintf('COM_MOKOJOOMSTORELOCATOR_IMPORT_RESULT', $result['imported'], $result['updated'], $result['skipped']),
|
||||
'success'
|
||||
);
|
||||
|
||||
$app->redirect(Route::_('index.php?option=com_mokojoomstorelocator&view=locations', false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and import a CSV file.
|
||||
*
|
||||
* Expected columns: title, address, city, state, postcode, country, latitude, longitude,
|
||||
* phone, email, website, hours, description
|
||||
*
|
||||
* @param string $filePath Path to the CSV file.
|
||||
* @param bool $geocodeOnImport Whether to geocode missing coordinates.
|
||||
* @param bool $updateExisting Whether to update existing records by title match.
|
||||
*
|
||||
* @return array ['imported' => int, 'updated' => int, 'skipped' => int]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function processCSV(string $filePath, bool $geocodeOnImport, bool $updateExisting): array
|
||||
{
|
||||
$handle = fopen($filePath, 'r');
|
||||
|
||||
if (!$handle)
|
||||
{
|
||||
return ['imported' => 0, 'updated' => 0, 'skipped' => 0];
|
||||
}
|
||||
|
||||
// Read header row
|
||||
$headers = fgetcsv($handle);
|
||||
|
||||
if (!$headers)
|
||||
{
|
||||
fclose($handle);
|
||||
|
||||
return ['imported' => 0, 'updated' => 0, 'skipped' => 0];
|
||||
}
|
||||
|
||||
$headers = array_map('strtolower', array_map('trim', $headers));
|
||||
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$geocoder = $geocodeOnImport ? new Geocoder() : null;
|
||||
$imported = 0;
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false)
|
||||
{
|
||||
if (count($row) < count($headers))
|
||||
{
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
|
||||
if (empty($data['title']))
|
||||
{
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for existing record
|
||||
$existingId = null;
|
||||
|
||||
if ($updateExisting)
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__mokojoomstorelocator_locations'))
|
||||
->where($db->quoteName('title') . ' = :title')
|
||||
->bind(':title', $data['title']);
|
||||
$db->setQuery($query);
|
||||
$existingId = $db->loadResult();
|
||||
}
|
||||
|
||||
// Geocode if needed
|
||||
if ($geocoder && empty($data['latitude']) && empty($data['longitude']))
|
||||
{
|
||||
$coords = $geocoder->geocode(
|
||||
$data['address'] ?? '',
|
||||
$data['city'] ?? '',
|
||||
$data['state'] ?? '',
|
||||
$data['postcode'] ?? '',
|
||||
$data['country'] ?? ''
|
||||
);
|
||||
|
||||
if ($coords)
|
||||
{
|
||||
$data['latitude'] = $coords['lat'];
|
||||
$data['longitude'] = $coords['lng'];
|
||||
}
|
||||
}
|
||||
|
||||
// Build table record
|
||||
$table = $this->getModel('Location')->getTable();
|
||||
|
||||
if ($existingId)
|
||||
{
|
||||
$table->load($existingId);
|
||||
}
|
||||
|
||||
$locationData = [
|
||||
'title' => $data['title'] ?? '',
|
||||
'address' => $data['address'] ?? '',
|
||||
'city' => $data['city'] ?? '',
|
||||
'state' => $data['state'] ?? '',
|
||||
'postcode' => $data['postcode'] ?? '',
|
||||
'country' => $data['country'] ?? '',
|
||||
'latitude' => !empty($data['latitude']) ? (float) $data['latitude'] : null,
|
||||
'longitude' => !empty($data['longitude']) ? (float) $data['longitude'] : null,
|
||||
'phone' => $data['phone'] ?? '',
|
||||
'email' => $data['email'] ?? '',
|
||||
'website' => $data['website'] ?? '',
|
||||
'hours' => $data['hours'] ?? '',
|
||||
'description' => $data['description'] ?? '',
|
||||
'published' => 1,
|
||||
];
|
||||
|
||||
if (!$table->bind($locationData))
|
||||
{
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$table->check())
|
||||
{
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$table->store())
|
||||
{
|
||||
Log::add('Import failed for: ' . $data['title'] . ' — ' . $table->getError(), Log::WARNING, 'com_mokojoomstorelocator');
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existingId)
|
||||
{
|
||||
$updated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
$imported++;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return ['imported' => $imported, 'updated' => $updated, 'skipped' => $skipped];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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\FormController;
|
||||
|
||||
/**
|
||||
* Controller for a single location record.
|
||||
*
|
||||
* Handles save, cancel, edit actions for the location edit form.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class LocationController extends FormController
|
||||
{
|
||||
/**
|
||||
* The prefix for the model class name.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $text_prefix = 'COM_MOKOJOOMSTORELOCATOR_LOCATION';
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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\AdminController;
|
||||
|
||||
/**
|
||||
* Controller for the locations list.
|
||||
*
|
||||
* Handles publish, unpublish, delete, and ordering actions on the list view.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class LocationsController extends AdminController
|
||||
{
|
||||
/**
|
||||
* Proxy for getModel.
|
||||
*
|
||||
* @param string $name The model name.
|
||||
* @param string $prefix The model prefix.
|
||||
* @param array $config Configuration array.
|
||||
*
|
||||
* @return \Joomla\CMS\MVC\Model\BaseDatabaseModel
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getModel($name = 'Location', $prefix = 'Administrator', $config = ['ignore_request' => true])
|
||||
{
|
||||
return parent::getModel($name, $prefix, $config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<?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\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
|
||||
/**
|
||||
* Controller for sample data operations.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class SampledataController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Download a CSV template file with column headers and a sample row.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function download(): void
|
||||
{
|
||||
Session::checkToken('get') or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
$app = Factory::getApplication();
|
||||
$app->setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
$app->setHeader('Content-Disposition', 'attachment; filename="store-locations-template.csv"');
|
||||
$app->sendHeaders();
|
||||
|
||||
$output = fopen('php://output', 'w');
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
|
||||
$headers = ['title', 'address', 'city', 'state', 'postcode', 'country', 'latitude', 'longitude', 'phone', 'email', 'website', 'hours', 'description'];
|
||||
fputcsv($output, $headers);
|
||||
|
||||
fputcsv($output, [
|
||||
'Moko HQ',
|
||||
'123 Main Street',
|
||||
'Nashville',
|
||||
'TN',
|
||||
'37201',
|
||||
'US',
|
||||
'36.1627',
|
||||
'-86.7816',
|
||||
'(615) 555-0100',
|
||||
'hello@example.com',
|
||||
'https://example.com',
|
||||
'Mon-Fri 9am-5pm',
|
||||
'Our main office location.',
|
||||
]);
|
||||
|
||||
fclose($output);
|
||||
$app->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject sample location data into the database for testing.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function inject(): void
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
$app = Factory::getApplication();
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$now = Factory::getDate()->toSql();
|
||||
$uid = Factory::getApplication()->getIdentity()->id ?? 0;
|
||||
|
||||
$samples = [
|
||||
[
|
||||
'title' => 'Downtown Nashville Store',
|
||||
'alias' => 'downtown-nashville-store',
|
||||
'description' => 'Our flagship location in the heart of downtown Nashville.',
|
||||
'address' => '200 Broadway',
|
||||
'city' => 'Nashville',
|
||||
'state' => 'TN',
|
||||
'postcode' => '37201',
|
||||
'country' => 'US',
|
||||
'latitude' => 36.1622,
|
||||
'longitude' => -86.7744,
|
||||
'phone' => '(615) 555-0101',
|
||||
'email' => 'downtown@example.com',
|
||||
'website' => 'https://example.com/downtown',
|
||||
'hours' => "Mon-Fri: 9am-7pm\nSat: 10am-6pm\nSun: 12pm-5pm",
|
||||
],
|
||||
[
|
||||
'title' => 'East Nashville Location',
|
||||
'alias' => 'east-nashville-location',
|
||||
'description' => 'Serving the East Nashville community with friendly service.',
|
||||
'address' => '1000 Main St',
|
||||
'city' => 'Nashville',
|
||||
'state' => 'TN',
|
||||
'postcode' => '37206',
|
||||
'country' => 'US',
|
||||
'latitude' => 36.1781,
|
||||
'longitude' => -86.7534,
|
||||
'phone' => '(615) 555-0102',
|
||||
'email' => 'east@example.com',
|
||||
'website' => 'https://example.com/east',
|
||||
'hours' => "Mon-Sat: 10am-8pm\nSun: Closed",
|
||||
],
|
||||
[
|
||||
'title' => 'Franklin Square',
|
||||
'alias' => 'franklin-square',
|
||||
'description' => 'Conveniently located in the Franklin town square.',
|
||||
'address' => '400 Main St',
|
||||
'city' => 'Franklin',
|
||||
'state' => 'TN',
|
||||
'postcode' => '37064',
|
||||
'country' => 'US',
|
||||
'latitude' => 35.9251,
|
||||
'longitude' => -86.8689,
|
||||
'phone' => '(615) 555-0103',
|
||||
'email' => 'franklin@example.com',
|
||||
'website' => 'https://example.com/franklin',
|
||||
'hours' => "Mon-Fri: 8am-6pm\nSat-Sun: 10am-4pm",
|
||||
],
|
||||
[
|
||||
'title' => 'Murfreesboro Plaza',
|
||||
'alias' => 'murfreesboro-plaza',
|
||||
'description' => 'Our newest location serving Rutherford County.',
|
||||
'address' => '1720 Old Fort Pkwy',
|
||||
'city' => 'Murfreesboro',
|
||||
'state' => 'TN',
|
||||
'postcode' => '37129',
|
||||
'country' => 'US',
|
||||
'latitude' => 35.8353,
|
||||
'longitude' => -86.4160,
|
||||
'phone' => '(615) 555-0104',
|
||||
'email' => 'murfreesboro@example.com',
|
||||
'website' => 'https://example.com/murfreesboro',
|
||||
'hours' => "Mon-Sat: 9am-9pm\nSun: 11am-6pm",
|
||||
],
|
||||
[
|
||||
'title' => 'Clarksville Center',
|
||||
'alias' => 'clarksville-center',
|
||||
'description' => 'Serving the Clarksville-Montgomery County area.',
|
||||
'address' => '2801 Wilma Rudolph Blvd',
|
||||
'city' => 'Clarksville',
|
||||
'state' => 'TN',
|
||||
'postcode' => '37040',
|
||||
'country' => 'US',
|
||||
'latitude' => 36.5843,
|
||||
'longitude' => -87.3199,
|
||||
'phone' => '(931) 555-0105',
|
||||
'email' => 'clarksville@example.com',
|
||||
'website' => 'https://example.com/clarksville',
|
||||
'hours' => "Mon-Fri: 9am-7pm\nSat: 10am-5pm\nSun: Closed",
|
||||
],
|
||||
[
|
||||
'title' => 'Chattanooga Riverfront',
|
||||
'alias' => 'chattanooga-riverfront',
|
||||
'description' => 'Located near the Tennessee Aquarium on the riverfront.',
|
||||
'address' => '1 Broad St',
|
||||
'city' => 'Chattanooga',
|
||||
'state' => 'TN',
|
||||
'postcode' => '37402',
|
||||
'country' => 'US',
|
||||
'latitude' => 35.0557,
|
||||
'longitude' => -85.3097,
|
||||
'phone' => '(423) 555-0106',
|
||||
'email' => 'chattanooga@example.com',
|
||||
'website' => 'https://example.com/chattanooga',
|
||||
'hours' => "Daily: 10am-8pm",
|
||||
],
|
||||
[
|
||||
'title' => 'Knoxville Market Square',
|
||||
'alias' => 'knoxville-market-square',
|
||||
'description' => 'In the heart of downtown Knoxville at Market Square.',
|
||||
'address' => '36 Market Square',
|
||||
'city' => 'Knoxville',
|
||||
'state' => 'TN',
|
||||
'postcode' => '37902',
|
||||
'country' => 'US',
|
||||
'latitude' => 35.9643,
|
||||
'longitude' => -83.9198,
|
||||
'phone' => '(865) 555-0107',
|
||||
'email' => 'knoxville@example.com',
|
||||
'website' => 'https://example.com/knoxville',
|
||||
'hours' => "Mon-Sat: 9am-8pm\nSun: 12pm-6pm",
|
||||
],
|
||||
[
|
||||
'title' => 'Memphis Beale Street',
|
||||
'alias' => 'memphis-beale-street',
|
||||
'description' => 'Right on iconic Beale Street in downtown Memphis.',
|
||||
'address' => '152 Beale St',
|
||||
'city' => 'Memphis',
|
||||
'state' => 'TN',
|
||||
'postcode' => '38103',
|
||||
'country' => 'US',
|
||||
'latitude' => 35.1393,
|
||||
'longitude' => -90.0530,
|
||||
'phone' => '(901) 555-0108',
|
||||
'email' => 'memphis@example.com',
|
||||
'website' => 'https://example.com/memphis',
|
||||
'hours' => "Mon-Thu: 10am-10pm\nFri-Sat: 10am-12am\nSun: 11am-8pm",
|
||||
],
|
||||
];
|
||||
|
||||
$inserted = 0;
|
||||
|
||||
foreach ($samples as $sample)
|
||||
{
|
||||
$sample['published'] = 1;
|
||||
$sample['ordering'] = $inserted + 1;
|
||||
$sample['params'] = '{}';
|
||||
$sample['image'] = '';
|
||||
$sample['catid'] = 0;
|
||||
$sample['created'] = $now;
|
||||
$sample['created_by'] = $uid;
|
||||
$sample['modified'] = $now;
|
||||
$sample['modified_by'] = $uid;
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__mokojoomstorelocator_locations'))
|
||||
->where($db->quoteName('alias') . ' = :alias')
|
||||
->bind(':alias', $sample['alias']);
|
||||
$db->setQuery($query);
|
||||
|
||||
if ($db->loadResult() > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->insert($db->quoteName('#__mokojoomstorelocator_locations'))
|
||||
->columns($db->quoteName(array_keys($sample)))
|
||||
->values(implode(',', array_map(function ($v) use ($db) {
|
||||
return $db->quote($v);
|
||||
}, array_values($sample))));
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
try
|
||||
{
|
||||
$db->execute();
|
||||
$inserted++;
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
Log::add('Sample data insert failed: ' . $e->getMessage(), Log::WARNING, 'com_mokojoomstorelocator');
|
||||
}
|
||||
}
|
||||
|
||||
$app->enqueueMessage(
|
||||
Text::sprintf('COM_MOKOJOOMSTORELOCATOR_SAMPLEDATA_INJECTED', $inserted),
|
||||
'success'
|
||||
);
|
||||
|
||||
$app->redirect(Route::_('index.php?option=com_mokojoomstorelocator&view=locations', false));
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -11,13 +11,15 @@ namespace Moko\Component\MokoJoomStoreLocator\Administrator\Extension;
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\MVCComponent;
|
||||
use Joomla\CMS\Component\Router\RouterServiceInterface;
|
||||
use Joomla\CMS\Component\Router\RouterServiceTrait;
|
||||
|
||||
/**
|
||||
* Component class for com_mokojoomstorelocator.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class MokoJoomStoreLocatorComponent extends MVCComponent
|
||||
class MokoJoomStoreLocatorComponent extends MVCComponent implements RouterServiceInterface
|
||||
{
|
||||
// TODO: Add boot(), getRouterRules(), or custom services as needed
|
||||
use RouterServiceTrait;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
<?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\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Http\HttpFactory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
/**
|
||||
* Geocoding helper — converts addresses to latitude/longitude coordinates.
|
||||
*
|
||||
* Supports Nominatim (OpenStreetMap, free, default) and Google Geocoding API.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Geocoder
|
||||
{
|
||||
/**
|
||||
* @var string Geocoding provider: 'nominatim' or 'google'.
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private string $provider;
|
||||
|
||||
/**
|
||||
* @var string Google Geocoding API key (only for google provider).
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private string $apiKey;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$params = ComponentHelper::getParams('com_mokojoomstorelocator');
|
||||
|
||||
$this->provider = $params->get('geocoder_provider', 'nominatim');
|
||||
$this->apiKey = $params->get('google_api_key', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Geocode an address string to coordinates.
|
||||
*
|
||||
* @param string $address Street address.
|
||||
* @param string $city City.
|
||||
* @param string $state State/province.
|
||||
* @param string $postcode Postal code.
|
||||
* @param string $country Country.
|
||||
*
|
||||
* @return array|null ['lat' => float, 'lng' => float] or null on failure.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function geocode(string $address = '', string $city = '', string $state = '', string $postcode = '', string $country = ''): ?array
|
||||
{
|
||||
$parts = array_filter([$address, $city, $state, $postcode, $country]);
|
||||
$query = implode(', ', $parts);
|
||||
|
||||
if (empty($query))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->provider === 'google' && $this->apiKey)
|
||||
{
|
||||
return $this->geocodeGoogle($query);
|
||||
}
|
||||
|
||||
return $this->geocodeNominatim($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch geocode multiple locations.
|
||||
*
|
||||
* @param array $locations Array of arrays with address fields.
|
||||
*
|
||||
* @return array Array of results indexed by input key: ['lat' => float, 'lng' => float] or null.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function batchGeocode(array $locations): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($locations as $key => $loc)
|
||||
{
|
||||
$results[$key] = $this->geocode(
|
||||
$loc['address'] ?? '',
|
||||
$loc['city'] ?? '',
|
||||
$loc['state'] ?? '',
|
||||
$loc['postcode'] ?? '',
|
||||
$loc['country'] ?? ''
|
||||
);
|
||||
|
||||
// Nominatim rate limit: max 1 request per second
|
||||
if ($this->provider === 'nominatim')
|
||||
{
|
||||
usleep(1100000);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geocode using OpenStreetMap Nominatim (free, no API key).
|
||||
*
|
||||
* @param string $query The full address string.
|
||||
*
|
||||
* @return array|null Coordinates or null.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function geocodeNominatim(string $query): ?array
|
||||
{
|
||||
$url = 'https://nominatim.openstreetmap.org/search?' . http_build_query([
|
||||
'q' => $query,
|
||||
'format' => 'json',
|
||||
'limit' => 1,
|
||||
]);
|
||||
|
||||
$result = $this->httpGet($url, [
|
||||
'User-Agent' => 'MokoJoomStoreLocator/1.0 (Joomla component)',
|
||||
'Accept' => 'application/json',
|
||||
]);
|
||||
|
||||
if ($result === null || empty($result))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$first = $result[0] ?? null;
|
||||
|
||||
if (!$first || !isset($first['lat'], $first['lon']))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'lat' => (float) $first['lat'],
|
||||
'lng' => (float) $first['lon'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Geocode using Google Geocoding API.
|
||||
*
|
||||
* @param string $query The full address string.
|
||||
*
|
||||
* @return array|null Coordinates or null.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function geocodeGoogle(string $query): ?array
|
||||
{
|
||||
$url = 'https://maps.googleapis.com/maps/api/geocode/json?' . http_build_query([
|
||||
'address' => $query,
|
||||
'key' => $this->apiKey,
|
||||
]);
|
||||
|
||||
$result = $this->httpGet($url);
|
||||
|
||||
if ($result === null || ($result['status'] ?? '') !== 'OK')
|
||||
{
|
||||
$status = $result['status'] ?? 'unknown';
|
||||
Log::add("Geocoder: Google returned status $status for: $query", Log::WARNING, 'com_mokojoomstorelocator');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$location = $result['results'][0]['geometry']['location'] ?? null;
|
||||
|
||||
if (!$location)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'lat' => (float) $location['lat'],
|
||||
'lng' => (float) $location['lng'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an HTTP GET request and return decoded JSON.
|
||||
*
|
||||
* @param string $url The URL to fetch.
|
||||
* @param array $headers Additional headers.
|
||||
*
|
||||
* @return array|null Decoded JSON or null on error.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function httpGet(string $url, array $headers = []): ?array
|
||||
{
|
||||
try
|
||||
{
|
||||
$http = HttpFactory::getHttp();
|
||||
$response = $http->get($url, $headers, 15);
|
||||
|
||||
if ($response->code !== 200)
|
||||
{
|
||||
Log::add("Geocoder: HTTP {$response->code} from $url", Log::WARNING, 'com_mokojoomstorelocator');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode($response->body, true);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
Log::add('Geocoder: ' . $e->getMessage(), Log::ERROR, 'com_mokojoomstorelocator');
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,10 +59,46 @@ class LocationsModel extends ListModel
|
||||
$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
|
||||
$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)');
|
||||
}
|
||||
|
||||
$catId = (int) $this->getState('filter.category_id');
|
||||
|
||||
if ($catId > 0)
|
||||
{
|
||||
$query->where($db->quoteName('a.catid') . ' = :catid')
|
||||
->bind(':catid', $catId, \Joomla\Database\ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
$search = $this->getState('filter.search');
|
||||
|
||||
if (!empty($search))
|
||||
{
|
||||
$search = '%' . trim($search) . '%';
|
||||
$query->where(
|
||||
'(' . $db->quoteName('a.title') . ' LIKE :search1'
|
||||
. ' OR ' . $db->quoteName('a.city') . ' LIKE :search2'
|
||||
. ' OR ' . $db->quoteName('a.address') . ' LIKE :search3'
|
||||
. ' OR ' . $db->quoteName('a.postcode') . ' LIKE :search4)'
|
||||
)
|
||||
->bind(':search1', $search)
|
||||
->bind(':search2', $search)
|
||||
->bind(':search3', $search)
|
||||
->bind(':search4', $search);
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -43,10 +43,69 @@ class LocationTable extends Table
|
||||
*/
|
||||
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
|
||||
if (empty($this->title))
|
||||
{
|
||||
$this->setError('A location title is required.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($this->alias))
|
||||
{
|
||||
$this->alias = $this->title;
|
||||
}
|
||||
|
||||
$this->alias = \Joomla\CMS\Filter\OutputFilter::stringURLSafe($this->alias);
|
||||
|
||||
if (empty($this->alias))
|
||||
{
|
||||
$this->alias = \Joomla\CMS\Factory::getDate()->format('Y-m-d-H-i-s');
|
||||
}
|
||||
|
||||
if ($this->latitude !== null && ($this->latitude < -90 || $this->latitude > 90))
|
||||
{
|
||||
$this->setError('Latitude must be between -90 and 90.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->longitude !== null && ($this->longitude < -180 || $this->longitude > 180))
|
||||
{
|
||||
$this->setError('Longitude must be between -180 and 180.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$now = \Joomla\CMS\Factory::getDate()->toSql();
|
||||
|
||||
if (empty($this->created) || $this->created === '0000-00-00 00:00:00')
|
||||
{
|
||||
$this->created = $now;
|
||||
$this->created_by = \Joomla\CMS\Factory::getApplication()->getIdentity()->id ?? 0;
|
||||
}
|
||||
|
||||
$this->modified = $now;
|
||||
$this->modified_by = \Joomla\CMS\Factory::getApplication()->getIdentity()->id ?? 0;
|
||||
|
||||
// Auto-geocode if address present but coordinates missing
|
||||
if ((empty($this->latitude) || empty($this->longitude))
|
||||
&& (!empty($this->address) || !empty($this->city)))
|
||||
{
|
||||
$geocoder = new \Moko\Component\MokoJoomStoreLocator\Administrator\Helper\Geocoder();
|
||||
$coords = $geocoder->geocode(
|
||||
$this->address ?? '',
|
||||
$this->city ?? '',
|
||||
$this->state ?? '',
|
||||
$this->postcode ?? '',
|
||||
$this->country ?? ''
|
||||
);
|
||||
|
||||
if ($coords)
|
||||
{
|
||||
$this->latitude = $coords['lat'];
|
||||
$this->longitude = $coords['lng'];
|
||||
}
|
||||
}
|
||||
|
||||
return parent::check();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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\Import;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
/**
|
||||
* Import view.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* Display the import form.
|
||||
*
|
||||
* @param string $tpl Template name.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
ToolbarHelper::title('Store Locator: Import Locations');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?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\Location;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
/**
|
||||
* Location edit view.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $form;
|
||||
|
||||
/**
|
||||
* @var object
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $item;
|
||||
|
||||
/**
|
||||
* Display the view.
|
||||
*
|
||||
* @param string $tpl Template name.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
$this->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()->getInput()->set('hidemainmenu', true);
|
||||
|
||||
$isNew = ($this->item->id == 0);
|
||||
|
||||
ToolbarHelper::title(
|
||||
$isNew ? 'Store Locator: New Location' : 'Store Locator: 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');
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ namespace Moko\Component\MokoJoomStoreLocator\Administrator\View\Locations;
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Session\Session;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
/**
|
||||
@@ -78,5 +80,15 @@ class HtmlView extends BaseHtmlView
|
||||
ToolbarHelper::publish('locations.publish', 'JTOOLBAR_PUBLISH', true);
|
||||
ToolbarHelper::unpublish('locations.unpublish', 'JTOOLBAR_UNPUBLISH', true);
|
||||
ToolbarHelper::deleteList('', 'locations.delete', 'JTOOLBAR_DELETE');
|
||||
|
||||
$toolbar = Toolbar::getInstance('toolbar');
|
||||
|
||||
$exportUrl = 'index.php?option=com_mokojoomstorelocator&task=export.execute&' . Session::getFormToken() . '=1';
|
||||
$toolbar->standardButton('download', 'COM_MOKOJOOMSTORELOCATOR_EXPORT_CSV', '')->icon('icon-download')->url($exportUrl);
|
||||
|
||||
$sampleUrl = 'index.php?option=com_mokojoomstorelocator&task=sampledata.inject&' . Session::getFormToken() . '=1';
|
||||
$toolbar->standardButton('lightning', 'COM_MOKOJOOMSTORELOCATOR_SAMPLEDATA_INJECT', '')->icon('icon-lightning')->url($sampleUrl);
|
||||
|
||||
ToolbarHelper::preferences('com_mokojoomstorelocator');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?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\Router\Route;
|
||||
?>
|
||||
<form action="<?php echo Route::_('index.php?option=com_mokojoomstorelocator&task=import.execute'); ?>"
|
||||
method="post" name="adminForm" id="adminForm" enctype="multipart/form-data">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_TITLE'); ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="import_file" class="form-label">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_FILE'); ?>
|
||||
</label>
|
||||
<input type="file" class="form-control" id="import_file" name="import_file" accept=".csv" required />
|
||||
<div class="form-text">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_FILE_DESC'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="geocode" name="geocode" value="1" />
|
||||
<label class="form-check-label" for="geocode">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_GEOCODE'); ?>
|
||||
</label>
|
||||
<div class="form-text">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_GEOCODE_DESC'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="update_existing" name="update_existing" value="1" />
|
||||
<label class="form-check-label" for="update_existing">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_UPDATE_EXISTING'); ?>
|
||||
</label>
|
||||
<div class="form-text">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_UPDATE_EXISTING_DESC'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="icon-upload" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_SUBMIT'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title"><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_FORMAT'); ?></h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_FORMAT_DESC'); ?></p>
|
||||
<code class="d-block p-2 bg-light rounded" style="font-size: 11px; overflow-x: auto;">
|
||||
title,address,city,state,postcode,country,latitude,longitude,phone,email,website,hours,description
|
||||
</code>
|
||||
<hr>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokojoomstorelocator&task=sampledata.download&' . Session::getFormToken() . '=1'); ?>"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<span class="icon-download" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_DOWNLOAD_TEMPLATE'); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php echo HTMLHelper::_('form.token'); ?>
|
||||
</form>
|
||||
@@ -0,0 +1,158 @@
|
||||
<?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\Location\HtmlView $this */
|
||||
|
||||
HTMLHelper::_('behavior.formvalidator');
|
||||
HTMLHelper::_('behavior.keepalive');
|
||||
|
||||
$wa = $this->getDocument()->getWebAssetManager();
|
||||
$wa->useScript('keepalive')
|
||||
->useScript('form.validate');
|
||||
?>
|
||||
<form action="<?php echo Route::_('index.php?option=com_mokojoomstorelocator&layout=edit&id=' . (int) $this->item->id); ?>"
|
||||
method="post" name="adminForm" id="adminForm" class="form-validate">
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.startTabSet', 'myTab', ['active' => 'details', 'recall' => true, 'breakpoint' => 768]); ?>
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'details', Text::_('JDETAILS')); ?>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<?php echo $this->form->renderField('title'); ?>
|
||||
<?php echo $this->form->renderField('alias'); ?>
|
||||
<?php echo $this->form->renderField('description'); ?>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<?php echo $this->form->renderField('published'); ?>
|
||||
<?php echo $this->form->renderField('id'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'address', Text::_('COM_MOKOJOOMSTORELOCATOR_FIELDSET_ADDRESS')); ?>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<?php echo $this->form->renderField('address'); ?>
|
||||
<?php echo $this->form->renderField('city'); ?>
|
||||
<?php echo $this->form->renderField('state'); ?>
|
||||
<?php echo $this->form->renderField('postcode'); ?>
|
||||
<?php echo $this->form->renderField('country'); ?>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title"><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_FIELDSET_COORDINATES'); ?></h4>
|
||||
<small class="text-muted">Click the map to set coordinates</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="coordinate-picker-map" style="height: 300px; border: 1px solid #dee2e6; border-radius: 4px;"></div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-6">
|
||||
<?php echo $this->form->renderField('latitude'); ?>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<?php echo $this->form->renderField('longitude'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'contact', Text::_('COM_MOKOJOOMSTORELOCATOR_FIELDSET_CONTACT')); ?>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<?php echo $this->form->renderField('phone'); ?>
|
||||
<?php echo $this->form->renderField('email'); ?>
|
||||
<?php echo $this->form->renderField('website'); ?>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<?php echo $this->form->renderField('hours'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'image', Text::_('COM_MOKOJOOMSTORELOCATOR_FIELDSET_IMAGE')); ?>
|
||||
<?php echo $this->form->renderField('image'); ?>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.endTabSet'); ?>
|
||||
|
||||
<input type="hidden" name="task" value="">
|
||||
<?php echo HTMLHelper::_('form.token'); ?>
|
||||
</form>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var latField = document.getElementById('jform_latitude');
|
||||
var lngField = document.getElementById('jform_longitude');
|
||||
var lat = parseFloat(latField.value) || 39.8283;
|
||||
var lng = parseFloat(lngField.value) || -98.5795;
|
||||
var zoom = (latField.value && lngField.value) ? 15 : 4;
|
||||
|
||||
var map = L.map('coordinate-picker-map').setView([lat, lng], zoom);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
var marker = null;
|
||||
|
||||
if (latField.value && lngField.value) {
|
||||
marker = L.marker([lat, lng]).addTo(map);
|
||||
}
|
||||
|
||||
map.on('click', function(e) {
|
||||
var clickLat = e.latlng.lat.toFixed(8);
|
||||
var clickLng = e.latlng.lng.toFixed(8);
|
||||
|
||||
latField.value = clickLat;
|
||||
lngField.value = clickLng;
|
||||
|
||||
if (marker) {
|
||||
marker.setLatLng(e.latlng);
|
||||
} else {
|
||||
marker = L.marker(e.latlng).addTo(map);
|
||||
}
|
||||
});
|
||||
|
||||
// Update marker when fields change manually
|
||||
function updateMarkerFromFields() {
|
||||
var newLat = parseFloat(latField.value);
|
||||
var newLng = parseFloat(lngField.value);
|
||||
if (!isNaN(newLat) && !isNaN(newLng)) {
|
||||
var latlng = L.latLng(newLat, newLng);
|
||||
if (marker) {
|
||||
marker.setLatLng(latlng);
|
||||
} else {
|
||||
marker = L.marker(latlng).addTo(map);
|
||||
}
|
||||
map.setView(latlng, 15);
|
||||
}
|
||||
}
|
||||
|
||||
latField.addEventListener('change', updateMarkerFromFields);
|
||||
lngField.addEventListener('change', updateMarkerFromFields);
|
||||
|
||||
// Fix map rendering in tabs
|
||||
document.querySelector('[data-bs-target="#address"]')?.addEventListener('shown.bs.tab', function() {
|
||||
map.invalidateSize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -54,7 +54,31 @@ use Joomla\CMS\Router\Route;
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- TODO: Render location rows -->
|
||||
<?php foreach ($this->items as $i => $item) : ?>
|
||||
<tr class="row<?php echo $i % 2; ?>">
|
||||
<td class="text-center">
|
||||
<?php echo HTMLHelper::_('grid.id', $i, $item->id, false, 'cid', 'cb', $item->title); ?>
|
||||
</td>
|
||||
<th scope="row">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokojoomstorelocator&task=location.edit&id=' . $item->id); ?>">
|
||||
<?php echo $this->escape($item->title); ?>
|
||||
</a>
|
||||
<div class="small"><?php echo Text::_('JFIELD_ALIAS_LABEL') . ': ' . $this->escape($item->alias); ?></div>
|
||||
</th>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<?php echo $this->escape($item->city); ?>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<?php echo $this->escape($item->state); ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php echo HTMLHelper::_('jgrid.published', $item->published, $i, 'locations.', true, 'cb'); ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php echo (int) $item->id; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -57,6 +57,43 @@
|
||||
<menu>COM_MOKOJOOMSTORELOCATOR</menu>
|
||||
<submenu>
|
||||
<menu link="option=com_mokojoomstorelocator&view=locations">COM_MOKOJOOMSTORELOCATOR_LOCATIONS</menu>
|
||||
<menu link="option=com_mokojoomstorelocator&view=import">COM_MOKOJOOMSTORELOCATOR_IMPORT</menu>
|
||||
</submenu>
|
||||
</administration>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="geocoding" label="COM_MOKOJOOMSTORELOCATOR_FIELDSET_GEOCODING">
|
||||
<field
|
||||
name="geocoder_provider"
|
||||
type="list"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_GEOCODER_PROVIDER"
|
||||
default="nominatim"
|
||||
>
|
||||
<option value="nominatim">OpenStreetMap (Nominatim) — Free</option>
|
||||
<option value="google">Google Geocoding API</option>
|
||||
</field>
|
||||
|
||||
<field
|
||||
name="google_api_key"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_GOOGLE_API_KEY"
|
||||
description="COM_MOKOJOOMSTORELOCATOR_FIELD_GOOGLE_API_KEY_DESC"
|
||||
showon="geocoder_provider:google"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="auto_geocode"
|
||||
type="radio"
|
||||
label="COM_MOKOJOOMSTORELOCATOR_FIELD_AUTO_GEOCODE"
|
||||
description="COM_MOKOJOOMSTORELOCATOR_FIELD_AUTO_GEOCODE_DESC"
|
||||
default="1"
|
||||
class="btn-group"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
; MokoJoomStoreLocator - 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="Store Locations"
|
||||
COM_MOKOJOOMSTORELOCATOR_NO_LOCATIONS="No locations found."
|
||||
COM_MOKOJOOMSTORELOCATOR_LOCATION_DETAIL="Location Detail"
|
||||
COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS="Get Directions"
|
||||
COM_MOKOJOOMSTORELOCATOR_BACK_TO_LOCATIONS="Back to All Locations"
|
||||
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"
|
||||
@@ -0,0 +1,12 @@
|
||||
<?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;
|
||||
|
||||
// Site service provider delegates to admin provider
|
||||
// The admin services/provider.php handles component registration for both sides
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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\Site\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
|
||||
/**
|
||||
* Default controller for the site 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';
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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\Site\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Model\ItemModel;
|
||||
use Joomla\Database\ParameterType;
|
||||
|
||||
/**
|
||||
* Single location model for the frontend detail view.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class LocationModel extends ItemModel
|
||||
{
|
||||
/**
|
||||
* Get a single location by ID.
|
||||
*
|
||||
* @param int $pk The location ID.
|
||||
*
|
||||
* @return object|false The location record or false.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getItem($pk = null)
|
||||
{
|
||||
$pk = $pk ?: (int) $this->getState('location.id');
|
||||
|
||||
if (!$pk)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
$query->select('a.*')
|
||||
->from($db->quoteName('#__mokojoomstorelocator_locations', 'a'))
|
||||
->where($db->quoteName('a.id') . ' = :id')
|
||||
->where($db->quoteName('a.published') . ' = 1')
|
||||
->bind(':id', $pk, ParameterType::INTEGER);
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?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\Site\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Model\ListModel;
|
||||
use Joomla\Database\QueryInterface;
|
||||
|
||||
/**
|
||||
* Locations list model for the site (frontend) view.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class LocationsModel extends ListModel
|
||||
{
|
||||
/**
|
||||
* 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'))
|
||||
->where($db->quoteName('a.published') . ' = 1');
|
||||
|
||||
$catId = (int) $this->getState('filter.category_id');
|
||||
|
||||
if ($catId > 0)
|
||||
{
|
||||
$query->where($db->quoteName('a.catid') . ' = :catid')
|
||||
->bind(':catid', $catId, \Joomla\Database\ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
$search = $this->getState('filter.search');
|
||||
|
||||
if (!empty($search))
|
||||
{
|
||||
$search = '%' . trim($search) . '%';
|
||||
$query->where(
|
||||
'(' . $db->quoteName('a.title') . ' LIKE :search1'
|
||||
. ' OR ' . $db->quoteName('a.city') . ' LIKE :search2'
|
||||
. ' OR ' . $db->quoteName('a.address') . ' LIKE :search3'
|
||||
. ' OR ' . $db->quoteName('a.postcode') . ' LIKE :search4)'
|
||||
)
|
||||
->bind(':search1', $search)
|
||||
->bind(':search2', $search)
|
||||
->bind(':search3', $search)
|
||||
->bind(':search4', $search);
|
||||
}
|
||||
|
||||
$city = $this->getState('filter.city');
|
||||
|
||||
if (!empty($city))
|
||||
{
|
||||
$query->where($db->quoteName('a.city') . ' = :city')
|
||||
->bind(':city', $city);
|
||||
}
|
||||
|
||||
$state = $this->getState('filter.state');
|
||||
|
||||
if (!empty($state))
|
||||
{
|
||||
$query->where($db->quoteName('a.state') . ' = :state')
|
||||
->bind(':state', $state);
|
||||
}
|
||||
|
||||
// Radius search using Haversine formula (bounding box pre-filter + SQL distance)
|
||||
$lat = (float) $this->getState('filter.latitude');
|
||||
$lng = (float) $this->getState('filter.longitude');
|
||||
$radius = (float) $this->getState('filter.radius');
|
||||
$unit = $this->getState('filter.radius_unit', 'miles');
|
||||
|
||||
if ($lat && $lng && $radius > 0)
|
||||
{
|
||||
// Earth radius: 3959 miles or 6371 km
|
||||
$earthRadius = ($unit === 'km') ? 6371 : 3959;
|
||||
|
||||
$query->where($db->quoteName('a.latitude') . ' IS NOT NULL')
|
||||
->where($db->quoteName('a.longitude') . ' IS NOT NULL');
|
||||
|
||||
// Haversine distance calculation
|
||||
$haversine = sprintf(
|
||||
'(%f * ACOS(COS(RADIANS(%f)) * COS(RADIANS(a.latitude))'
|
||||
. ' * COS(RADIANS(a.longitude) - RADIANS(%f))'
|
||||
. ' + SIN(RADIANS(%f)) * SIN(RADIANS(a.latitude))))',
|
||||
$earthRadius,
|
||||
$lat,
|
||||
$lng,
|
||||
$lat
|
||||
);
|
||||
|
||||
$query->select($haversine . ' AS distance')
|
||||
->having('distance <= ' . (float) $radius)
|
||||
->order('distance ASC');
|
||||
}
|
||||
else
|
||||
{
|
||||
$query->order($db->quoteName('a.ordering') . ' ASC');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?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\Site\Service;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Application\SiteApplication;
|
||||
use Joomla\CMS\Component\Router\RouterView;
|
||||
use Joomla\CMS\Component\Router\RouterViewConfiguration;
|
||||
use Joomla\CMS\Component\Router\Rules\MenuRules;
|
||||
use Joomla\CMS\Component\Router\Rules\NomenuRules;
|
||||
use Joomla\CMS\Component\Router\Rules\StandardRules;
|
||||
use Joomla\CMS\Menu\AbstractMenu;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* SEF URL router for com_mokojoomstorelocator.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Router extends RouterView
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param SiteApplication $app The application object.
|
||||
* @param AbstractMenu $menu The menu object.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(SiteApplication $app, AbstractMenu $menu)
|
||||
{
|
||||
// Locations list view
|
||||
$locations = new RouterViewConfiguration('locations');
|
||||
$this->registerView($locations);
|
||||
|
||||
// Single location view
|
||||
$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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the segment for a location.
|
||||
*
|
||||
* @param string $id The ID with alias (e.g., "5:my-store").
|
||||
* @param array $query The request query.
|
||||
*
|
||||
* @return array The segment.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getLocationSegment($id, $query): array
|
||||
{
|
||||
if (strpos($id, ':') === false)
|
||||
{
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$dbQuery = $db->getQuery(true)
|
||||
->select($db->quoteName('alias'))
|
||||
->from($db->quoteName('#__mokojoomstorelocator_locations'))
|
||||
->where($db->quoteName('id') . ' = :id')
|
||||
->bind(':id', $id, \Joomla\Database\ParameterType::INTEGER);
|
||||
$db->setQuery($dbQuery);
|
||||
$alias = $db->loadResult();
|
||||
|
||||
if ($alias)
|
||||
{
|
||||
$id = $id . ':' . $alias;
|
||||
}
|
||||
}
|
||||
|
||||
[$numericId, $alias] = explode(':', $id, 2) + [1 => ''];
|
||||
|
||||
return [$numericId => $alias ?: $numericId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID for a location segment.
|
||||
*
|
||||
* @param string $segment The URL segment.
|
||||
* @param array $query The request query.
|
||||
*
|
||||
* @return int|false The location ID or false.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getLocationId($segment, $query): int|false
|
||||
{
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$dbQuery = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__mokojoomstorelocator_locations'))
|
||||
->where($db->quoteName('alias') . ' = :alias')
|
||||
->bind(':alias', $segment);
|
||||
$db->setQuery($dbQuery);
|
||||
$id = $db->loadResult();
|
||||
|
||||
return $id ? (int) $id : (int) $segment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?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\Site\View\Location;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
/**
|
||||
* Single location detail view for the frontend.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* @var object The location item.
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $item;
|
||||
|
||||
/**
|
||||
* Display the view.
|
||||
*
|
||||
* @param string $tpl Template name.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
$this->item = $this->get('Item');
|
||||
|
||||
if (!$this->item)
|
||||
{
|
||||
throw new \Exception('Location not found', 404);
|
||||
}
|
||||
|
||||
// Set page title
|
||||
$this->getDocument()->setTitle($this->item->title);
|
||||
|
||||
// Add Schema.org structured data
|
||||
$this->addStructuredData();
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Schema.org LocalBusiness JSON-LD to the document.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected function addStructuredData(): void
|
||||
{
|
||||
$item = $this->item;
|
||||
|
||||
$schema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'LocalBusiness',
|
||||
'name' => $item->title,
|
||||
];
|
||||
|
||||
if ($item->description)
|
||||
{
|
||||
$schema['description'] = strip_tags($item->description);
|
||||
}
|
||||
|
||||
$address = [];
|
||||
|
||||
if ($item->address)
|
||||
{
|
||||
$address['streetAddress'] = $item->address;
|
||||
}
|
||||
|
||||
if ($item->city)
|
||||
{
|
||||
$address['addressLocality'] = $item->city;
|
||||
}
|
||||
|
||||
if ($item->state)
|
||||
{
|
||||
$address['addressRegion'] = $item->state;
|
||||
}
|
||||
|
||||
if ($item->postcode)
|
||||
{
|
||||
$address['postalCode'] = $item->postcode;
|
||||
}
|
||||
|
||||
if ($item->country)
|
||||
{
|
||||
$address['addressCountry'] = $item->country;
|
||||
}
|
||||
|
||||
if (!empty($address))
|
||||
{
|
||||
$address['@type'] = 'PostalAddress';
|
||||
$schema['address'] = $address;
|
||||
}
|
||||
|
||||
if ($item->latitude && $item->longitude)
|
||||
{
|
||||
$schema['geo'] = [
|
||||
'@type' => 'GeoCoordinates',
|
||||
'latitude' => (float) $item->latitude,
|
||||
'longitude' => (float) $item->longitude,
|
||||
];
|
||||
}
|
||||
|
||||
if ($item->phone)
|
||||
{
|
||||
$schema['telephone'] = $item->phone;
|
||||
}
|
||||
|
||||
if ($item->email)
|
||||
{
|
||||
$schema['email'] = $item->email;
|
||||
}
|
||||
|
||||
if ($item->website)
|
||||
{
|
||||
$schema['url'] = $item->website;
|
||||
}
|
||||
|
||||
if ($item->image)
|
||||
{
|
||||
$schema['image'] = $item->image;
|
||||
}
|
||||
|
||||
$this->getDocument()->addScriptOptions('com_mokojoomstorelocator.schema', $schema);
|
||||
$this->getDocument()->addCustomTag(
|
||||
'<script type="application/ld+json">' . json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . '</script>'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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\Site\View\Locations;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
/**
|
||||
* Locations list view for the frontend.
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?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\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
/** @var \Moko\Component\MokoJoomStoreLocator\Site\View\Location\HtmlView $this */
|
||||
$item = $this->item;
|
||||
?>
|
||||
<div class="com-mokojoomstorelocator-location" itemscope itemtype="https://schema.org/LocalBusiness">
|
||||
<h2 itemprop="name"><?php echo $this->escape($item->title); ?></h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-7">
|
||||
<?php if ($item->image) : ?>
|
||||
<div class="mokojoomstorelocator-location-image mb-3">
|
||||
<img src="<?php echo $this->escape($item->image); ?>"
|
||||
alt="<?php echo $this->escape($item->title); ?>"
|
||||
itemprop="image" class="img-fluid rounded" loading="lazy" />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->description) : ?>
|
||||
<div class="mokojoomstorelocator-location-description mb-3" itemprop="description">
|
||||
<?php echo $item->description; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mokojoomstorelocator-location-details card mb-3">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title"><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_FIELDSET_CONTACT'); ?></h4>
|
||||
|
||||
<?php if ($item->address || $item->city || $item->state) : ?>
|
||||
<div class="mb-2" itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
|
||||
<strong><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_FIELDSET_ADDRESS'); ?>:</strong><br>
|
||||
<?php if ($item->address) : ?>
|
||||
<span itemprop="streetAddress"><?php echo $this->escape($item->address); ?></span><br>
|
||||
<?php endif; ?>
|
||||
<?php if ($item->city) : ?>
|
||||
<span itemprop="addressLocality"><?php echo $this->escape($item->city); ?></span>,
|
||||
<?php endif; ?>
|
||||
<?php if ($item->state) : ?>
|
||||
<span itemprop="addressRegion"><?php echo $this->escape($item->state); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($item->postcode) : ?>
|
||||
<span itemprop="postalCode"><?php echo $this->escape($item->postcode); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($item->country) : ?>
|
||||
<br><span itemprop="addressCountry"><?php echo $this->escape($item->country); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->phone) : ?>
|
||||
<div class="mb-2">
|
||||
<strong><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_FIELD_PHONE'); ?>:</strong>
|
||||
<a href="tel:<?php echo $this->escape($item->phone); ?>" itemprop="telephone">
|
||||
<?php echo $this->escape($item->phone); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->email) : ?>
|
||||
<div class="mb-2">
|
||||
<strong><?php echo Text::_('JGLOBAL_EMAIL'); ?>:</strong>
|
||||
<a href="mailto:<?php echo $this->escape($item->email); ?>" itemprop="email">
|
||||
<?php echo $this->escape($item->email); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->website) : ?>
|
||||
<div class="mb-2">
|
||||
<strong><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_FIELD_WEBSITE'); ?>:</strong>
|
||||
<a href="<?php echo $this->escape($item->website); ?>" itemprop="url" target="_blank" rel="noopener">
|
||||
<?php echo $this->escape(parse_url($item->website, PHP_URL_HOST) ?: $item->website); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->hours) : ?>
|
||||
<div class="mb-2">
|
||||
<strong><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_FIELD_HOURS'); ?>:</strong><br>
|
||||
<span itemprop="openingHours"><?php echo nl2br($this->escape($item->hours)); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($item->latitude && $item->longitude) : ?>
|
||||
<div class="mb-3">
|
||||
<a href="https://www.google.com/maps/dir/?api=1&destination=<?php echo (float) $item->latitude; ?>,<?php echo (float) $item->longitude; ?>"
|
||||
class="btn btn-primary" target="_blank" rel="noopener">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<?php if ($item->latitude && $item->longitude) : ?>
|
||||
<div id="mokojoomstorelocator-detail-map" style="height: 350px; border-radius: 8px;"></div>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var lat = <?php echo (float) $item->latitude; ?>;
|
||||
var lng = <?php echo (float) $item->longitude; ?>;
|
||||
var map = L.map('mokojoomstorelocator-detail-map').setView([lat, lng], 15);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
L.marker([lat, lng]).addTo(map)
|
||||
.bindPopup('<strong><?php echo $this->escape($item->title); ?></strong>')
|
||||
.openPopup();
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokojoomstorelocator&view=locations'); ?>" class="btn btn-outline-secondary">
|
||||
← <?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_BACK_TO_LOCATIONS'); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
/** @var \Moko\Component\MokoJoomStoreLocator\Site\View\Locations\HtmlView $this */
|
||||
?>
|
||||
<div class="com-mokojoomstorelocator-locations">
|
||||
<h2><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_LOCATIONS'); ?></h2>
|
||||
|
||||
<?php if (empty($this->items)) : ?>
|
||||
<p><?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?></p>
|
||||
<?php else : ?>
|
||||
<div class="mokojoomstorelocator-list">
|
||||
<?php foreach ($this->items as $item) : ?>
|
||||
<div class="mokojoomstorelocator-location" itemscope itemtype="https://schema.org/LocalBusiness">
|
||||
<h3 itemprop="name">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokojoomstorelocator&view=location&id=' . $item->id . ':' . $item->alias); ?>">
|
||||
<?php echo $this->escape($item->title); ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<div class="mokojoomstorelocator-address" itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
|
||||
<?php if ($item->address) : ?>
|
||||
<span itemprop="streetAddress"><?php echo $this->escape($item->address); ?></span><br>
|
||||
<?php endif; ?>
|
||||
<?php if ($item->city) : ?>
|
||||
<span itemprop="addressLocality"><?php echo $this->escape($item->city); ?></span>,
|
||||
<?php endif; ?>
|
||||
<?php if ($item->state) : ?>
|
||||
<span itemprop="addressRegion"><?php echo $this->escape($item->state); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($item->postcode) : ?>
|
||||
<span itemprop="postalCode"><?php echo $this->escape($item->postcode); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($item->phone) : ?>
|
||||
<div class="mokojoomstorelocator-phone">
|
||||
<a href="tel:<?php echo $this->escape($item->phone); ?>" itemprop="telephone">
|
||||
<?php echo $this->escape($item->phone); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->website) : ?>
|
||||
<div class="mokojoomstorelocator-website">
|
||||
<a href="<?php echo $this->escape($item->website); ?>" itemprop="url" target="_blank" rel="noopener">
|
||||
<?php echo $this->escape(parse_url($item->website, PHP_URL_HOST) ?: $item->website); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->hours) : ?>
|
||||
<div class="mokojoomstorelocator-hours" itemprop="openingHours">
|
||||
<?php echo nl2br($this->escape($item->hours)); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->image) : ?>
|
||||
<div class="mokojoomstorelocator-image">
|
||||
<img src="<?php echo $this->escape($item->image); ?>"
|
||||
alt="<?php echo $this->escape($item->title); ?>"
|
||||
itemprop="image" loading="lazy" />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($item->distance)) : ?>
|
||||
<div class="mokojoomstorelocator-distance">
|
||||
<small class="text-muted"><?php echo number_format($item->distance, 1); ?> away</small>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item->latitude && $item->longitude) : ?>
|
||||
<div class="mokojoomstorelocator-directions">
|
||||
<a href="https://www.google.com/maps/dir/?api=1&destination=<?php echo (float) $item->latitude; ?>,<?php echo (float) $item->longitude; ?>"
|
||||
target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php echo $this->pagination->getListFooter(); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -34,9 +34,43 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
|
||||
{
|
||||
$data = parent::getLayoutData();
|
||||
|
||||
// TODO: Load published locations with coordinates from the component table
|
||||
// TODO: Build marker data array for the map JS
|
||||
$data['locations'] = [];
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$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('#__mokojoomstorelocator_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;
|
||||
}
|
||||
|
||||
@@ -16,15 +16,159 @@ $locations = $displayData['locations'] ?? [];
|
||||
$mapHeight = $params->get('map_height', '400px');
|
||||
$mapZoom = (int) $params->get('map_zoom', 10);
|
||||
$provider = $params->get('map_provider', 'leaflet');
|
||||
$apiKey = $params->get('api_key', '');
|
||||
$moduleId = $displayData['module']->id;
|
||||
?>
|
||||
<div class="mod-mokojoomstorelocator-map"
|
||||
id="mokojoomstorelocator-map-<?php echo $displayData['module']->id; ?>"
|
||||
id="mokojoomstorelocator-map-<?php echo $moduleId; ?>"
|
||||
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>
|
||||
|
||||
<?php if ($provider === 'leaflet') : ?>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" crossorigin="" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" crossorigin="" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js" crossorigin=""></script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($provider === 'google' && $apiKey) : ?>
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=<?php echo $this->escape($apiKey); ?>&libraries=marker&callback=Function.prototype" async defer></script>
|
||||
<script src="https://unpkg.com/@googlemaps/markerclusterer@2.5.3/dist/index.min.js"></script>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var container = document.getElementById('mokojoomstorelocator-map-<?php echo $moduleId; ?>');
|
||||
var locations = <?php echo json_encode($locations); ?>;
|
||||
var defaultZoom = <?php echo $mapZoom; ?>;
|
||||
var provider = '<?php echo $this->escape($provider); ?>';
|
||||
|
||||
if (!locations.length) {
|
||||
var msg = document.createElement('p');
|
||||
msg.style.cssText = 'text-align:center;padding:2em;color:#6c757d;';
|
||||
msg.textContent = '<?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS', true); ?>';
|
||||
container.appendChild(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate center from locations
|
||||
var avgLat = 0, avgLng = 0;
|
||||
locations.forEach(function(loc) { avgLat += loc.lat; avgLng += loc.lng; });
|
||||
avgLat /= locations.length;
|
||||
avgLng /= locations.length;
|
||||
|
||||
if (provider === 'leaflet') {
|
||||
initLeaflet(container, locations, avgLat, avgLng, defaultZoom);
|
||||
} else if (provider === 'google') {
|
||||
initGoogle(container, locations, avgLat, avgLng, defaultZoom);
|
||||
}
|
||||
});
|
||||
|
||||
function buildPopupContent(loc) {
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'mokojoomstorelocator-popup';
|
||||
|
||||
var title = document.createElement('strong');
|
||||
title.textContent = loc.title;
|
||||
wrapper.appendChild(title);
|
||||
|
||||
if (loc.address) {
|
||||
wrapper.appendChild(document.createElement('br'));
|
||||
var addr = document.createElement('small');
|
||||
addr.textContent = loc.address;
|
||||
wrapper.appendChild(addr);
|
||||
}
|
||||
|
||||
if (loc.phone) {
|
||||
wrapper.appendChild(document.createElement('br'));
|
||||
var phoneLink = document.createElement('a');
|
||||
phoneLink.href = 'tel:' + loc.phone;
|
||||
phoneLink.textContent = loc.phone;
|
||||
wrapper.appendChild(phoneLink);
|
||||
}
|
||||
|
||||
wrapper.appendChild(document.createElement('br'));
|
||||
var dirLink = document.createElement('a');
|
||||
dirLink.href = 'https://www.google.com/maps/dir/?api=1&destination=' + loc.lat + ',' + loc.lng;
|
||||
dirLink.target = '_blank';
|
||||
dirLink.rel = 'noopener';
|
||||
dirLink.textContent = '<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS', true); ?>';
|
||||
wrapper.appendChild(dirLink);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function initLeaflet(container, locations, centerLat, centerLng, zoom) {
|
||||
var map = L.map(container).setView([centerLat, centerLng], zoom);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
var markers = L.markerClusterGroup();
|
||||
|
||||
locations.forEach(function(loc) {
|
||||
var marker = L.marker([loc.lat, loc.lng]);
|
||||
var popup = L.popup().setContent(buildPopupContent(loc));
|
||||
marker.bindPopup(popup);
|
||||
markers.addLayer(marker);
|
||||
});
|
||||
|
||||
map.addLayer(markers);
|
||||
|
||||
// Fit bounds to show all markers
|
||||
if (locations.length > 1) {
|
||||
var bounds = markers.getBounds();
|
||||
map.fitBounds(bounds, { padding: [30, 30] });
|
||||
}
|
||||
}
|
||||
|
||||
function initGoogle(container, locations, centerLat, centerLng, zoom) {
|
||||
var map = new google.maps.Map(container, {
|
||||
center: { lat: centerLat, lng: centerLng },
|
||||
zoom: zoom
|
||||
});
|
||||
|
||||
var infoWindow = new google.maps.InfoWindow();
|
||||
var gMarkers = [];
|
||||
|
||||
locations.forEach(function(loc) {
|
||||
var marker = new google.maps.Marker({
|
||||
position: { lat: loc.lat, lng: loc.lng },
|
||||
map: map,
|
||||
title: loc.title
|
||||
});
|
||||
|
||||
marker.addListener('click', function() {
|
||||
infoWindow.setContent(buildPopupContent(loc));
|
||||
infoWindow.open(map, marker);
|
||||
});
|
||||
|
||||
gMarkers.push(marker);
|
||||
});
|
||||
|
||||
if (typeof markerClusterer !== 'undefined') {
|
||||
new markerClusterer.MarkerClusterer({ map: map, markers: gMarkers });
|
||||
}
|
||||
|
||||
// Fit bounds
|
||||
if (locations.length > 1) {
|
||||
var bounds = new google.maps.LatLngBounds();
|
||||
locations.forEach(function(loc) {
|
||||
bounds.extend({ lat: loc.lat, lng: loc.lng });
|
||||
});
|
||||
map.fitBounds(bounds);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.mokojoomstorelocator-popup { font-size: 13px; line-height: 1.5; }
|
||||
.mokojoomstorelocator-popup strong { font-size: 14px; }
|
||||
</style>
|
||||
|
||||
+8
@@ -11,3 +11,11 @@ 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_USE_LOCATION="Use My Location"
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH_LOCATING="Finding your location..."
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH_LOCATION_FOUND="Location found! Select a radius and search."
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_DENIED="Location access denied. Please enable location services."
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_UNAVAILABLE="Location information unavailable."
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_TIMEOUT="Location request timed out."
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_ERROR="Unable to determine your location."
|
||||
|
||||
@@ -34,8 +34,34 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
|
||||
{
|
||||
$data = parent::getLayoutData();
|
||||
|
||||
// TODO: Load distinct cities/states for filter dropdowns
|
||||
// TODO: Build radius options array from params
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$table = $db->quoteName('#__mokojoomstorelocator_locations');
|
||||
|
||||
// Load distinct cities
|
||||
$query = $db->getQuery(true)
|
||||
->select('DISTINCT ' . $db->quoteName('city'))
|
||||
->from($table)
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where($db->quoteName('city') . ' != ' . $db->quote(''))
|
||||
->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($table)
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where($db->quoteName('state') . ' != ' . $db->quote(''))
|
||||
->order($db->quoteName('state') . ' ASC');
|
||||
$db->setQuery($query);
|
||||
$data['states'] = $db->loadColumn();
|
||||
|
||||
// Parse radius options from params
|
||||
$params = $data['params'];
|
||||
$radiusString = $params->get('radius_options', '5,10,25,50,100');
|
||||
$data['radius_options'] = array_map('intval', explode(',', $radiusString));
|
||||
$data['radius_unit'] = $params->get('radius_unit', 'miles');
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -12,32 +12,143 @@ use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
/** @var array $displayData */
|
||||
$params = $displayData['params'];
|
||||
$params = $displayData['params'];
|
||||
$moduleId = $displayData['module']->id;
|
||||
?>
|
||||
<div class="mod-mokojoomstorelocator-search">
|
||||
<div class="mod-mokojoomstorelocator-search" id="mokojoomstorelocator-search-<?php echo $moduleId; ?>">
|
||||
<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">
|
||||
<label for="mokojoomstorelocator-query-<?php echo $moduleId; ?>">
|
||||
<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_LABEL'); ?>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="mokojoomstorelocator-query"
|
||||
id="mokojoomstorelocator-query-<?php echo $moduleId; ?>"
|
||||
name="filter_search"
|
||||
class="form-control"
|
||||
placeholder="<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_PLACEHOLDER'); ?>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- TODO: City dropdown filter -->
|
||||
<!-- TODO: Radius dropdown filter -->
|
||||
<!-- TODO: Geolocation "Use my location" button -->
|
||||
<?php if ($params->get('show_city_filter', 1) && !empty($displayData['cities'])) : ?>
|
||||
<div class="mokojoomstorelocator-search-field mt-2">
|
||||
<label for="mokojoomstorelocator-city-<?php echo $moduleId; ?>">
|
||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_FIELD_CITY'); ?>
|
||||
</label>
|
||||
<select id="mokojoomstorelocator-city-<?php echo $moduleId; ?>" name="filter_city" class="form-select">
|
||||
<option value=""><?php echo Text::_('JALL'); ?></option>
|
||||
<?php foreach ($displayData['cities'] as $city) : ?>
|
||||
<option value="<?php echo $this->escape($city); ?>"><?php echo $this->escape($city); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<?php echo Text::_('JSEARCH_FILTER_SUBMIT'); ?>
|
||||
</button>
|
||||
<?php if ($params->get('show_radius_filter', 1) && !empty($displayData['radius_options'])) : ?>
|
||||
<div class="mokojoomstorelocator-search-field mt-2">
|
||||
<label for="mokojoomstorelocator-radius-<?php echo $moduleId; ?>">
|
||||
<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS_UNIT'); ?>
|
||||
</label>
|
||||
<select id="mokojoomstorelocator-radius-<?php echo $moduleId; ?>" name="filter_radius" class="form-select">
|
||||
<option value=""><?php echo Text::_('JALL'); ?></option>
|
||||
<?php foreach ($displayData['radius_options'] as $r) : ?>
|
||||
<option value="<?php echo (int) $r; ?>"><?php echo (int) $r; ?> <?php echo $this->escape($displayData['radius_unit']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" id="mokojoomstorelocator-lat-<?php echo $moduleId; ?>" name="filter_latitude" value="" />
|
||||
<input type="hidden" id="mokojoomstorelocator-lng-<?php echo $moduleId; ?>" name="filter_longitude" value="" />
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mokojoomstorelocator-search-actions mt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<?php echo Text::_('JSEARCH_FILTER_SUBMIT'); ?>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary ms-2"
|
||||
id="mokojoomstorelocator-geolocate-<?php echo $moduleId; ?>"
|
||||
title="<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_USE_LOCATION'); ?>">
|
||||
<span class="icon-location" aria-hidden="true"></span>
|
||||
<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_USE_LOCATION'); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="mokojoomstorelocator-geostatus-<?php echo $moduleId; ?>"
|
||||
class="mokojoomstorelocator-geostatus mt-2" style="display:none;"></div>
|
||||
|
||||
<input type="hidden" name="option" value="com_mokojoomstorelocator" />
|
||||
<input type="hidden" name="view" value="locations" />
|
||||
<input type="hidden" name="filter_radius_unit" value="<?php echo $this->escape($displayData['radius_unit'] ?? 'miles'); ?>" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var moduleId = '<?php echo $moduleId; ?>';
|
||||
var geoBtn = document.getElementById('mokojoomstorelocator-geolocate-' + moduleId);
|
||||
var statusEl = document.getElementById('mokojoomstorelocator-geostatus-' + moduleId);
|
||||
var latField = document.getElementById('mokojoomstorelocator-lat-' + moduleId);
|
||||
var lngField = document.getElementById('mokojoomstorelocator-lng-' + moduleId);
|
||||
var radiusEl = document.getElementById('mokojoomstorelocator-radius-' + moduleId);
|
||||
|
||||
if (!geoBtn) return;
|
||||
|
||||
if (!navigator.geolocation) {
|
||||
geoBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
geoBtn.addEventListener('click', function() {
|
||||
geoBtn.disabled = true;
|
||||
showStatus('<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_LOCATING', true); ?>', 'info');
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
var lat = position.coords.latitude;
|
||||
var lng = position.coords.longitude;
|
||||
|
||||
if (latField) latField.value = lat.toFixed(8);
|
||||
if (lngField) lngField.value = lng.toFixed(8);
|
||||
|
||||
// Auto-select first radius if none selected
|
||||
if (radiusEl && !radiusEl.value && radiusEl.options.length > 1) {
|
||||
radiusEl.selectedIndex = 1;
|
||||
}
|
||||
|
||||
showStatus('<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_LOCATION_FOUND', true); ?>', 'success');
|
||||
geoBtn.disabled = false;
|
||||
},
|
||||
function(error) {
|
||||
var msg;
|
||||
switch (error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
msg = '<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_DENIED', true); ?>';
|
||||
break;
|
||||
case error.POSITION_UNAVAILABLE:
|
||||
msg = '<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_UNAVAILABLE', true); ?>';
|
||||
break;
|
||||
case error.TIMEOUT:
|
||||
msg = '<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_TIMEOUT', true); ?>';
|
||||
break;
|
||||
default:
|
||||
msg = '<?php echo Text::_('MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_ERROR', true); ?>';
|
||||
}
|
||||
showStatus(msg, 'warning');
|
||||
geoBtn.disabled = false;
|
||||
},
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 300000 }
|
||||
);
|
||||
});
|
||||
|
||||
function showStatus(message, type) {
|
||||
statusEl.style.display = 'block';
|
||||
statusEl.className = 'mokojoomstorelocator-geostatus mt-2 alert alert-' + type;
|
||||
statusEl.textContent = message;
|
||||
if (type === 'success') {
|
||||
setTimeout(function() { statusEl.style.display = 'none'; }, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
+42
-2
@@ -85,7 +85,10 @@ class Pkg_MokojoomstorelocatorInstallerScript
|
||||
*/
|
||||
public function postflight($type, $parent)
|
||||
{
|
||||
// TODO: Post-installation tasks (enable modules, set defaults, etc.)
|
||||
if ($type === 'install')
|
||||
{
|
||||
$this->enableModules();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -101,6 +104,43 @@ class Pkg_MokojoomstorelocatorInstallerScript
|
||||
*/
|
||||
public function uninstall($parent)
|
||||
{
|
||||
// TODO: Cleanup tasks on uninstall
|
||||
Log::add('MokoJoomStoreLocator package uninstalled.', Log::INFO, 'jerror');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the bundled modules after first install.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function enableModules(): void
|
||||
{
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
|
||||
$modules = [
|
||||
'mod_mokojoomstorelocator_map',
|
||||
'mod_mokojoomstorelocator_search',
|
||||
];
|
||||
|
||||
foreach ($modules as $module)
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('element') . ' = :element')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('module'))
|
||||
->bind(':element', $module);
|
||||
$db->setQuery($query);
|
||||
|
||||
try
|
||||
{
|
||||
$db->execute();
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
Log::add('Failed to enable ' . $module . ': ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user