From b6202a6a40d8c6ccd88e115f1ec89ee419390bf0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 28 Jun 2026 11:37:23 -0500 Subject: [PATCH] feat: add visual post calendar admin view (#160) Add a monthly calendar grid view to the admin component showing scheduled, queued, and posted cross-posts with color-coded status badges. Includes month-by-month navigation and today highlighting. New files: - CalendarController, CalendarModel, Calendar HtmlView, calendar template Modified files: - MokoSuiteCrossHelper: added Calendar to submenu - Language file: added calendar strings - CHANGELOG.md: documented new feature Authored-by: Moko Consulting --- CHANGELOG.md | 2 + .../src/Controller/CalendarController.php | 24 ++++ .../src/Model/CalendarModel.php | 67 +++++++++ .../src/View/Calendar/HtmlView.php | 65 +++++++++ .../tmpl/calendar/default.php | 129 ++++++++++++++++++ 5 files changed, 287 insertions(+) create mode 100644 source/packages/com_mokosuitecross/src/Controller/CalendarController.php create mode 100644 source/packages/com_mokosuitecross/src/Model/CalendarModel.php create mode 100644 source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php create mode 100644 source/packages/com_mokosuitecross/tmpl/calendar/default.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6de93027..e8116d30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] ### Added +- **Visual post calendar**: Monthly calendar grid view showing scheduled, queued, and posted cross-posts with status badges (#160) +- **Calendar navigation**: Month-by-month navigation with today highlighting (#160) - **Social image generator**: Generate Open Graph images with article title overlay using PHP GD library (#157) - **Social image config**: Background color, text color, overlay style, and site name override in component options (#157) - **AI caption generation**: Generate platform-optimized cross-post captions from article content using Claude or OpenAI (#161) diff --git a/source/packages/com_mokosuitecross/src/Controller/CalendarController.php b/source/packages/com_mokosuitecross/src/Controller/CalendarController.php new file mode 100644 index 00000000..e36b117e --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Controller/CalendarController.php @@ -0,0 +1,24 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace Joomla\Component\MokoSuiteCross\Administrator\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Controller\BaseController; + +class CalendarController extends BaseController +{ + public function display($cachable = false, $urlparams = []): static + { + return parent::display($cachable, $urlparams); + } +} diff --git a/source/packages/com_mokosuitecross/src/Model/CalendarModel.php b/source/packages/com_mokosuitecross/src/Model/CalendarModel.php new file mode 100644 index 00000000..1ecd02f2 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Model/CalendarModel.php @@ -0,0 +1,67 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace Joomla\Component\MokoSuiteCross\Administrator\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Model\BaseDatabaseModel; + +class CalendarModel extends BaseDatabaseModel +{ + /** + * Get cross-post events for a given month, grouped by date. + * + * @param int $year Four-digit year + * @param int $month Month number (1-12) + * + * @return array Associative array keyed by Y-m-d, each value an array of event objects + */ + public function getEvents(int $year, int $month): array + { + $db = $this->getDatabase(); + + $firstDay = sprintf('%04d-%02d-01', $year, $month); + $lastDay = date('Y-m-t', strtotime($firstDay)); + + $dateExpr = 'COALESCE(' + . $db->quoteName('p.scheduled_at') . ', ' + . $db->quoteName('p.posted_at') . ', ' + . $db->quoteName('p.created') . ')'; + + $query = $db->getQuery(true) + ->select([ + 'DATE(' . $dateExpr . ') AS event_date', + $db->quoteName('p.status'), + $db->quoteName('s.service_type'), + $db->quoteName('c.title', 'article_title'), + ]) + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('LEFT', $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) + ->join('LEFT', $db->quoteName('#__content', 'c') + . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('p.article_id')) + ->where('DATE(' . $dateExpr . ') >= ' . $db->quote($firstDay)) + ->where('DATE(' . $dateExpr . ') <= ' . $db->quote($lastDay)) + ->order('DATE(' . $dateExpr . ') ASC, ' . $db->quoteName('p.created') . ' ASC'); + + $db->setQuery($query); + $rows = $db->loadObjectList() ?: []; + + $grouped = []; + + foreach ($rows as $row) { + $grouped[$row->event_date][] = $row; + } + + return $grouped; + } +} diff --git a/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php new file mode 100644 index 00000000..58706228 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php @@ -0,0 +1,65 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace Joomla\Component\MokoSuiteCross\Administrator\View\Calendar; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper; + +class HtmlView extends BaseHtmlView +{ + public int $year; + public int $month; + public array $events; + public $sidebar; + + public function display($tpl = null): void + { + $input = Factory::getApplication()->input; + + $this->year = $input->getInt('year', (int) date('Y')); + $this->month = $input->getInt('month', (int) date('n')); + + if ($this->month < 1 || $this->month > 12) { + $this->month = (int) date('n'); + } + + if ($this->year < 2000 || $this->year > 2100) { + $this->year = (int) date('Y'); + } + + $model = $this->getModel(); + $this->events = $model->getEvents($this->year, $this->month); + + $this->addToolbar(); + + MokoSuiteCrossHelper::addSubmenu('calendar'); + $this->sidebar = \Joomla\CMS\HTML\Sidebar::render(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + $canDo = MokoSuiteCrossHelper::getActions(); + + ToolbarHelper::title('MokoSuiteCross -- Post Calendar', 'calendar'); + ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuitecross&view=dashboard'); + + if ($canDo->get('core.admin')) { + ToolbarHelper::preferences('com_mokosuitecross'); + } + } +} diff --git a/source/packages/com_mokosuitecross/tmpl/calendar/default.php b/source/packages/com_mokosuitecross/tmpl/calendar/default.php new file mode 100644 index 00000000..dc0e2187 --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/calendar/default.php @@ -0,0 +1,129 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Calendar\HtmlView $this */ + +$year = $this->year; +$month = $this->month; +$events = $this->events; +$today = date('Y-m-d'); + +$prevMonth = $month - 1; +$prevYear = $year; + +if ($prevMonth < 1) { + $prevMonth = 12; + $prevYear--; +} + +$nextMonth = $month + 1; +$nextYear = $year; + +if ($nextMonth > 12) { + $nextMonth = 1; + $nextYear++; +} + +$monthName = date('F', mktime(0, 0, 0, $month, 1, $year)); +$daysInMonth = (int) date('t', mktime(0, 0, 0, $month, 1, $year)); +$firstWeekday = ((int) date('N', mktime(0, 0, 0, $month, 1, $year))) - 1; + +$statusClass = static function (string $status): string { + return match ($status) { + 'posted' => 'bg-success', + 'failed' => 'bg-danger', + default => 'bg-warning text-dark', + }; +}; +?> + +
+ + + + +

+ + + + +
+ +
+ + + + + + + + + + + + + + + + + + $daysInMonth) : ?> + + + + + + + +
   +
+ + + + +
+ + + service_type)); ?>: + article_title, 0, 20)); ?> + + +
+
-- 2.52.0