feat: visual post calendar admin view (#160) #198
@@ -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)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteCross
|
||||
* @subpackage com_mokosuitecross
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteCross
|
||||
* @subpackage com_mokosuitecross
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteCross
|
||||
* @subpackage com_mokosuitecross
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteCross
|
||||
* @subpackage com_mokosuitecross
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @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',
|
||||
};
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitecross&view=calendar&year=' . $prevYear . '&month=' . $prevMonth); ?>"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<span class="icon-chevron-left" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOSUITECROSS_CALENDAR_PREV_MONTH'); ?>
|
||||
</a>
|
||||
<h3 class="mb-0"><?php echo htmlspecialchars($monthName . ' ' . $year); ?></h3>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitecross&view=calendar&year=' . $nextYear . '&month=' . $nextMonth); ?>"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<?php echo Text::_('COM_MOKOSUITECROSS_CALENDAR_NEXT_MONTH'); ?>
|
||||
<span class="icon-chevron-right" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:14.28%"><?php echo Text::_('MON'); ?></th>
|
||||
<th style="width:14.28%"><?php echo Text::_('TUE'); ?></th>
|
||||
<th style="width:14.28%"><?php echo Text::_('WED'); ?></th>
|
||||
<th style="width:14.28%"><?php echo Text::_('THU'); ?></th>
|
||||
<th style="width:14.28%"><?php echo Text::_('FRI'); ?></th>
|
||||
<th style="width:14.28%"><?php echo Text::_('SAT'); ?></th>
|
||||
<th style="width:14.28%"><?php echo Text::_('SUN'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$day = 1;
|
||||
$started = false;
|
||||
|
||||
while ($day <= $daysInMonth) : ?>
|
||||
<tr>
|
||||
<?php for ($col = 0; $col < 7; $col++) :
|
||||
if (!$started && $col < $firstWeekday) : ?>
|
||||
<td class="text-muted bg-light"> </td>
|
||||
<?php
|
||||
continue;
|
||||
endif;
|
||||
|
||||
$started = true;
|
||||
|
||||
if ($day > $daysInMonth) : ?>
|
||||
<td class="text-muted bg-light"> </td>
|
||||
<?php
|
||||
continue;
|
||||
endif;
|
||||
|
||||
$dateKey = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
$isToday = ($dateKey === $today);
|
||||
$cellClass = $isToday ? 'border border-primary border-2 bg-primary bg-opacity-10' : '';
|
||||
$dayEvents = $events[$dateKey] ?? [];
|
||||
?>
|
||||
<td class="<?php echo $cellClass; ?>" style="vertical-align: top; min-height: 80px;">
|
||||
<div class="fw-bold mb-1<?php echo $isToday ? ' text-primary' : ''; ?>">
|
||||
<?php echo $day; ?>
|
||||
<?php if ($isToday) : ?>
|
||||
<small class="text-primary"><?php echo Text::_('COM_MOKOSUITECROSS_CALENDAR_TODAY'); ?></small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php foreach ($dayEvents as $event) : ?>
|
||||
<span class="badge <?php echo $statusClass($event->status); ?> mb-1 d-block text-truncate" style="max-width: 100%;"
|
||||
title="<?php echo htmlspecialchars(ucfirst($event->service_type) . ': ' . $event->article_title . ' (' . $event->status . ')'); ?>">
|
||||
<?php echo htmlspecialchars(ucfirst($event->service_type)); ?>:
|
||||
<?php echo htmlspecialchars(mb_substr($event->article_title, 0, 20)); ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</td>
|
||||
<?php
|
||||
$day++;
|
||||
endfor; ?>
|
||||
</tr>
|
||||
<?php endwhile; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
Reference in New Issue
Block a user