From a53713283656d95eb3e10ca2ec151debb04c918f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 25 Jun 2026 08:20:15 -0500 Subject: [PATCH] feat(#133): add site frontend with cross-post list and detail views Authored-by: Moko Consulting --- .../language/en-GB/com_mokosuitecross.ini | 11 ++- .../site/src/Controller/DisplayController.php | 2 +- .../site/src/Model/PostModel.php | 64 ++++++++++++++ .../site/src/Model/PostsModel.php | 51 +++++++++++ .../site/src/View/Post/HtmlView.php | 33 ++++++++ .../site/src/View/Posts/HtmlView.php | 30 +++++++ .../site/tmpl/post/default.php | 84 +++++++++++++++++++ .../site/tmpl/posts/default.php | 66 +++++++++++++++ 8 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 source/packages/com_mokosuitecross/site/src/Model/PostModel.php create mode 100644 source/packages/com_mokosuitecross/site/src/Model/PostsModel.php create mode 100644 source/packages/com_mokosuitecross/site/src/View/Post/HtmlView.php create mode 100644 source/packages/com_mokosuitecross/site/src/View/Posts/HtmlView.php create mode 100644 source/packages/com_mokosuitecross/site/tmpl/post/default.php create mode 100644 source/packages/com_mokosuitecross/site/tmpl/posts/default.php diff --git a/source/packages/com_mokosuitecross/site/language/en-GB/com_mokosuitecross.ini b/source/packages/com_mokosuitecross/site/language/en-GB/com_mokosuitecross.ini index 2ffcc30c..b69ad850 100644 --- a/source/packages/com_mokosuitecross/site/language/en-GB/com_mokosuitecross.ini +++ b/source/packages/com_mokosuitecross/site/language/en-GB/com_mokosuitecross.ini @@ -1,5 +1,14 @@ -; MokoSuiteCross — Site Frontend Language File +; MokoSuiteCross -- Site Frontend Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later COM_MOKOSUITECROSS="MokoSuiteCross" +COM_MOKOSUITECROSS_POSTS_LIST_TITLE="Cross-Posted Content" +COM_MOKOSUITECROSS_POST_DETAIL_TITLE="Cross-Post History" +COM_MOKOSUITECROSS_COLUMN_ARTICLE="Article" +COM_MOKOSUITECROSS_COLUMN_PLATFORMS="Platforms" +COM_MOKOSUITECROSS_COLUMN_LAST_POSTED="Last Posted" +COM_MOKOSUITECROSS_COLUMN_STATUS="Status" +COM_MOKOSUITECROSS_COLUMN_POSTED_DATE="Posted Date" +COM_MOKOSUITECROSS_COLUMN_LINK="Platform Link" +COM_MOKOSUITECROSS_NO_POSTS="No cross-posted content found." diff --git a/source/packages/com_mokosuitecross/site/src/Controller/DisplayController.php b/source/packages/com_mokosuitecross/site/src/Controller/DisplayController.php index be00f933..9c9415f2 100644 --- a/source/packages/com_mokosuitecross/site/src/Controller/DisplayController.php +++ b/source/packages/com_mokosuitecross/site/src/Controller/DisplayController.php @@ -17,5 +17,5 @@ use Joomla\CMS\MVC\Controller\BaseController; class DisplayController extends BaseController { - protected $default_view = 'post'; + protected $default_view = 'posts'; } diff --git a/source/packages/com_mokosuitecross/site/src/Model/PostModel.php b/source/packages/com_mokosuitecross/site/src/Model/PostModel.php new file mode 100644 index 00000000..b709d82c --- /dev/null +++ b/source/packages/com_mokosuitecross/site/src/Model/PostModel.php @@ -0,0 +1,64 @@ + + * @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\Site\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\BaseDatabaseModel; + +class PostModel extends BaseDatabaseModel +{ + public function getArticle(int $articleId): ?object + { + $db = $this->getDatabase(); + $user = Factory::getApplication()->getIdentity(); + + $query = $db->getQuery(true) + ->select('a.id, a.title, a.alias, a.catid, a.access') + ->from($db->quoteName('#__content', 'a')) + ->where('a.id = ' . (int) $articleId) + ->where('a.state = 1'); + + $groups = $user->getAuthorisedViewLevels(); + $query->where('a.access IN (' . implode(',', array_map('intval', $groups)) . ')'); + + $db->setQuery($query); + + return $db->loadObject() ?: null; + } + + public function getPosts(int $articleId): array + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select([ + 'p.id', + 'p.status', + 'p.platform_post_id', + 'p.posted_at', + 'p.error_message', + 'p.created', + 's.title AS service_title', + 's.service_type', + ]) + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON s.id = p.service_id') + ->where('p.article_id = ' . (int) $articleId) + ->order('p.created DESC'); + + $db->setQuery($query, 0, 50); + + return $db->loadObjectList() ?: []; + } +} diff --git a/source/packages/com_mokosuitecross/site/src/Model/PostsModel.php b/source/packages/com_mokosuitecross/site/src/Model/PostsModel.php new file mode 100644 index 00000000..eaef7684 --- /dev/null +++ b/source/packages/com_mokosuitecross/site/src/Model/PostsModel.php @@ -0,0 +1,51 @@ + + * @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\Site\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\ListModel; + +class PostsModel extends ListModel +{ + protected function getListQuery() + { + $db = $this->getDatabase(); + $user = Factory::getApplication()->getIdentity(); + $query = $db->getQuery(true); + + $query->select([ + 'a.id AS article_id', + 'a.title AS article_title', + 'a.alias AS article_alias', + 'a.catid', + 'MAX(p.posted_at) AS last_posted', + 'COUNT(p.id) AS post_count', + 'GROUP_CONCAT(DISTINCT s.service_type ORDER BY s.service_type SEPARATOR \',\') AS service_types', + ]) + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__content', 'a') . ' ON a.id = p.article_id') + ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON s.id = p.service_id') + ->where('p.status = ' . $db->quote('posted')) + ->where('a.state = 1'); + + // Access filtering + $groups = $user->getAuthorisedViewLevels(); + $query->where('a.access IN (' . implode(',', array_map('intval', $groups)) . ')'); + + $query->group('a.id, a.title, a.alias, a.catid') + ->order('last_posted DESC'); + + return $query; + } +} diff --git a/source/packages/com_mokosuitecross/site/src/View/Post/HtmlView.php b/source/packages/com_mokosuitecross/site/src/View/Post/HtmlView.php new file mode 100644 index 00000000..b4729330 --- /dev/null +++ b/source/packages/com_mokosuitecross/site/src/View/Post/HtmlView.php @@ -0,0 +1,33 @@ + + * @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\Site\View\Post; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; + +class HtmlView extends BaseHtmlView +{ + protected $article; + protected $posts; + + public function display($tpl = null): void + { + $articleId = Factory::getApplication()->getInput()->getInt('id', 0); + $model = $this->getModel(); + $this->article = $model->getArticle($articleId); + $this->posts = $model->getPosts($articleId); + + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitecross/site/src/View/Posts/HtmlView.php b/source/packages/com_mokosuitecross/site/src/View/Posts/HtmlView.php new file mode 100644 index 00000000..ac262d5b --- /dev/null +++ b/source/packages/com_mokosuitecross/site/src/View/Posts/HtmlView.php @@ -0,0 +1,30 @@ + + * @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\Site\View\Posts; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; + +class HtmlView extends BaseHtmlView +{ + protected $items; + protected $pagination; + + public function display($tpl = null): void + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + + parent::display($tpl); + } +} diff --git a/source/packages/com_mokosuitecross/site/tmpl/post/default.php b/source/packages/com_mokosuitecross/site/tmpl/post/default.php new file mode 100644 index 00000000..5b95ce9a --- /dev/null +++ b/source/packages/com_mokosuitecross/site/tmpl/post/default.php @@ -0,0 +1,84 @@ + + * @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; + +$statusClasses = [ + 'posted' => 'bg-success', + 'failed' => 'bg-danger', + 'permanently_failed' => 'bg-danger', + 'queued' => 'bg-warning text-dark', + 'posting' => 'bg-info', + 'scheduled' => 'bg-primary', + 'deleted' => 'bg-secondary', + 'cancelled' => 'bg-secondary', +]; + +?> +
+ article) : ?> +
+ +
+ +

+

+ escape($this->article->title); ?> +

+ + posts)) : ?> +
+ +
+ + + + + + + + + + + + posts as $post) : ?> + + + + + + + + +
+ escape($post->service_type); ?> + escape($post->service_title); ?> + + + escape(ucfirst($post->status)); ?> + + posted_at ? $this->escape($post->posted_at) : $this->escape($post->created); ?> + platform_post_id)) : ?> + escape($post->platform_post_id); ?> + + — + +
+ + + + ← + + +
diff --git a/source/packages/com_mokosuitecross/site/tmpl/posts/default.php b/source/packages/com_mokosuitecross/site/tmpl/posts/default.php new file mode 100644 index 00000000..2acd7b40 --- /dev/null +++ b/source/packages/com_mokosuitecross/site/tmpl/posts/default.php @@ -0,0 +1,66 @@ + + * @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; + +?> +
+

+ + items)) : ?> +
+ +
+ + + + + + + + + + + items as $item) : ?> + + + + + + + +
+ + escape($item->article_title); ?> + + + service_types ?? ''); + foreach ($types as $type) : + $type = trim($type); + if (empty($type)) continue; + ?> + escape($type); ?> + + + last_posted ? $this->escape($item->last_posted) : '—'; ?> +
+ + pagination->pagesTotal > 1) : ?> +
+ pagination->getListFooter(); ?> +
+ + +
-- 2.52.0