From c68aefb847b09daa0697de7684d3451e2e68ce1e Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 09 Jul 2026 23:10:23 +0000
Subject: [PATCH] =Refactor of Integrations.php to be a bit more useful with a suite of methods for create, update, delete back and forths for integrations where sharing is enabled. Also considering a single instance with a connect method to connect as the site (no user) vs connecting as an individual user - rather than recreating instances for every user.

---
 inc/blocks/FeedBlock.php |  914 ++++++++++++++++++++++++++++++++++++--------------------
 1 files changed, 580 insertions(+), 334 deletions(-)

diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 3693d93..223c554 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -2,6 +2,7 @@
 namespace JVBase\blocks;
 
 use JVBase\managers\Cache;
+use JVBase\meta\Meta;
 use JVBase\registrar\Registrar;
 use JVBase\base\Site;
 use JVBase\forms\TaxonomySelector;
@@ -25,6 +26,9 @@
 	protected ?int $contextID = null;
 
 	protected bool $isGallery = false;
+	protected array $args;
+	protected bool $isContentTax = false;
+	protected bool $hasMore = false;
 
 	public function __construct()
 	{
@@ -33,6 +37,7 @@
 		if (JVB_TESTING) {
 			$this->cache->flush();
 		}
+		$this->cache->flush();
 
 		add_action('init', [$this, 'registerBlock']);
 	}
@@ -78,15 +83,26 @@
 	protected function determineContent(array $attrs):void
 	{
 		if (array_key_exists('inheritQuery', $attrs) && $attrs['inheritQuery'] === true) {
-			if (is_post_type_archive()) {
+			$args = [
+				'posts_per_page'	=> 36,
+				'fields'			=> 'ids',
+			];
+			if (is_post_type_archive() &&!is_tax()) {
 				$obj = get_queried_object();
+				$registrar = Registrar::getInstance($obj->name);
+				if ($registrar && $registrar->hasFeature('is_timeline')) {
+					$args['post_parent'] = 0;
+				}
 				$this->content = [jvbNoBase($obj->name)];
+				$args['post_type'] = $obj->name;
+				$this->args = $args;
 				return;
 			} elseif (!empty(Registrar::getProfileTypes()) && is_singular(Registrar::getProfileTypes())) {
 				global $post;
 				$author = $post->post_author;
 				$role = jvbUserRole($author);
 
+				$args['post_author'] = $author;
 				$this->context = jvbNoBase($role);
 				$this->contextID = $author;
 				$registrar = Registrar::getInstance($role);
@@ -94,38 +110,80 @@
 					return;
 				}
 				$this->content = $registrar->getCreatable();
+				$args['post_type'] = array_map('jvbCheckBase', $this->content);
+				foreach($args['post_type'] as $post_type) {
+					$reg = Registrar::getInstance($post_type);
+					if ($reg && $reg->hasFeature('is_timeline')) {
+						$args['post_parent'] = 0;
+					}
+				}
+				$this->args = $args;
 				return;
 			} elseif (is_tax()) {
 				$obj = get_queried_object();
 				$this->context = jvbNoBase($obj->taxonomy);
 				$this->contextID = $obj->term_id;
+				$args['tax_query'] = [];
+				$args['tax_query'][] = [
+					'taxonomy'	=> $obj->taxonomy,
+					'terms'		=> $obj->term_id,
+				];
 				$registrar = Registrar::getInstance($obj->taxonomy);
 				if (!$registrar) {
 					return;
 				}
 				if ($registrar->hasFeature('is_content')) {
-					//example: tattoo shop, etc TODO
+					$this->isContentTax = true;
+					$this->content = [$registrar->getBased()];
 					return;
 				}
 				$this->content = array_map(function ($item) { return jvbNoBase($item); }, $registrar->registrar->for);
+				$args['post_type'] = array_map('jvbCheckBase', $registrar->registrar->for);
+				foreach($args['post_type'] as $post_type) {
+					$reg = Registrar::getInstance($post_type);
+					if ($reg && $reg->hasFeature('is_timeline')) {
+						$args['post_parent'] = 0;
+					}
+				}
+				$this->args = $args;
 				return;
 			}
 		}
 		// not inheriting, getting from config
 		$this->content = $attrs['contentTypes']??[];
-	}
-		protected function getContent():string
-		{
-			return implode(',', $this->content);
-		}
-		protected function determineTaxonomies():void
-		{
-			$taxonomies = [];
-			$ignore = [];
-			foreach ($this->content as $content) {
 
-				$registrar = Registrar::getInstance($content);
-				if (!$registrar) continue;
+		$args['post_type'] = array_map('jvbCheckBase', $attrs['contentTypes']);
+		$this->args = $args;
+	}
+	protected function getContent():string
+	{
+		return implode(',', $this->content);
+	}
+	protected function determineTaxonomies():void
+	{
+		$taxonomies = [];
+		$ignore = [];
+		foreach ($this->content as $content) {
+
+			$registrar = Registrar::getInstance($content);
+			if (!$registrar) continue;
+			if ($registrar->hasFeature('is_content')) {
+				foreach ($registrar->registrar->for as $c) {
+					$contentRegistrar = Registrar::getInstance($c);
+					if (!$contentRegistrar) continue;
+					$theTax = $contentRegistrar->registrar->taxonomies;
+					foreach ($theTax as $tax) {
+						if (!in_array($tax, $ignore) && !in_array($tax, $taxonomies)) {
+							$taxReg = Registrar::getInstance($tax);
+							if ($taxReg->hasFeature('show_feed')) {
+								$taxonomies[] = $tax;
+							} else {
+								$ignore[] = $tax;
+							}
+						}
+					}
+				}
+			} else {
 				$theTax = $registrar->registrar->taxonomies;
 				foreach ($theTax as $tax) {
 					if (!in_array($tax, $ignore) && !in_array($tax, $taxonomies)) {
@@ -138,8 +196,9 @@
 					}
 				}
 			}
-			$this->taxonomies = array_unique($taxonomies);
 		}
+		$this->taxonomies = array_unique($taxonomies);
+	}
 
 	protected function renderFiltersAndControls():string
 	{
@@ -158,42 +217,42 @@
 			jvbIcon('x')
 		);
 	}
-		protected function renderSearch():string
-		{
-			return sprintf(
-				'<div class="search row left nowrap">
+	protected function renderSearch():string
+	{
+		return sprintf(
+			'<div class="search row left nowrap">
 				<span class="label">Search:</span>
 				%s
 				</div>',
-				jvbSearch()
-			);
-		}
-		protected function renderContentLabels():string
-		{
-			return sprintf(
-				'<span class="label">Showing: <span class="current">%s</span></span>',
-				$this->content[0]??''
-			);
+			jvbSearch()
+		);
+	}
+	protected function renderContentLabels():string
+	{
+		return sprintf(
+			'<span class="label">Showing: <span class="current">%s</span></span>',
+			$this->content[0]??''
+		);
 
-		}
-		protected function renderContent():string
-		{
-			$favourites = '';
-			if (Site::has('favourites')) {
-				$favourites = sprintf(
-					'<input type="checkbox" id="favourites" class="btn" name="favourites" value="on" data-filter="favourites">
+	}
+	protected function renderContent():string
+	{
+		$favourites = '';
+		if (Site::has('favourites')) {
+			$favourites = sprintf(
+				'<input type="checkbox" id="favourites" class="btn" name="favourites" value="on" data-filter="favourites">
 					<label for="favourites" title="Show Favourites">%s%s<span class="screen-reader-text">Show Favourites Only</span></label>',
-					jvbIcon('heart'),
-					jvbIcon('heart', ['style' => 'fill'])
-				);
-			}
-			if (count($this->content) === 1) {
-				return empty($favourites)
-					? sprintf(
-						'<input type="hidden" data-filter="content" name="content" value="%s">',
-						implode(',', $this->content)
-					)
-					: sprintf(
+				jvbIcon('heart'),
+				jvbIcon('heart', ['style' => 'fill'])
+			);
+		}
+		if (count($this->content) === 1) {
+			return empty($favourites)
+				? sprintf(
+					'<input type="hidden" data-filter="content" name="content" value="%s">',
+					implode(',', $this->content)
+				)
+				: sprintf(
 					'<div class="content row right">
 						<input type="hidden" name="content" value="%s">
 						%s
@@ -201,42 +260,42 @@
 					implode(',', $this->content),
 					$favourites
 				);
+		}
+		$i = 0;
+		$content = implode('', array_map(function($type) use (&$i) {
+			$registrar = Registrar::getInstance($type);
+
+			$args = [
+				'post_type'	=> $registrar->getBased(),
+				'posts_per_page'	=> 1,
+				'fields'	=> 'ids',
+			];
+			if (!is_null($this->context)) {
+				$context = Registrar::getInstance($this->context);
+				switch ($context->getType()) {
+					case 'term':
+						$args['tax_query'] = [];
+						$args['tax_query'][] = [
+							'taxonomy'	=> $context->getBased(),
+							'terms'		=> $this->contextID
+						];
+						break;
+					case 'user':
+						$args['author'] = $this->contextID;
+						break;
+				}
 			}
-			$i = 0;
-			$content = implode('', array_map(function($type) use (&$i) {
-				$registrar = Registrar::getInstance($type);
+			$check = new WP_Query($args);
+			$hasPosts = !empty($check->posts);
+			wp_reset_postdata();
 
-				$args = [
-					'post_type'	=> $registrar->getBased(),
-					'posts_per_page'	=> 1,
-					'fields'	=> 'ids',
-				];
-				if (!is_null($this->context)) {
-
-					$context = Registrar::getInstance($this->context);
-					switch ($context->getType()) {
-						case 'term':
-							$args['tax_query'] = [];
-							$args['tax_query'][] = [
-								'taxonomy'	=> $context->getBased(),
-								'terms'		=> $this->contextID
-							];
-							break;
-						case 'user':
-							$args['author'] = $this->contextID;
-							break;
-					}
-				}
-				$check = new WP_Query($args);
-				$hasPosts = !empty($check->posts);
-
-				$disabled = !$hasPosts;
-				$checked = $i === 0 && $hasPosts;
-				if ($hasPosts) {
-					$i++;
-				}
-				return sprintf(
-					'<input type="radio"
+			$disabled = !$hasPosts;
+			$checked = $i === 0 && $hasPosts;
+			if ($hasPosts) {
+				$i++;
+			}
+			return sprintf(
+				'<input type="radio"
 					id="filter-%s"
 					class="btn"
 					name="content"
@@ -244,60 +303,60 @@
 					data-label="%s"
 					value="%s"%s%s>
 					<label for="filter-%s" title="Show %s">%s<span class="label">%s</span></label>',
-					$type,
-					$registrar->getSingular(),
-					$type,
-					$checked ? ' checked' : '',
-					$disabled ? ' disabled' : '',
-					$type,
-					$registrar->getSingular(),
-					jvbIcon($registrar->getIcon()),
-					$registrar->getSingular()
-				);
-			}, $this->content));
+				$type,
+				$registrar->getSingular(),
+				$type,
+				$checked ? ' checked' : '',
+				$disabled ? ' disabled' : '',
+				$type,
+				$registrar->getSingular(),
+				jvbIcon($registrar->getIcon()),
+				$registrar->getSingular()
+			);
+		}, $this->content));
 
 
 
-			return sprintf(
-				'<div class="content row left nowrap"><span class="label">Showing:</span>
+		return sprintf(
+			'<div class="content row left nowrap"><span class="label">Showing:</span>
 					%s%s
 				</div>',
-				$content,
-				$favourites
-			);
+			$content,
+			$favourites
+		);
+	}
+
+	protected function renderFilters():string
+	{
+		if (empty ($this->taxonomies)) {
+			return '';
 		}
+		$inside = implode('', array_filter(array_map(function($tax) {
+			$registrar = Registrar::getInstance($tax);
+			if (!$registrar) return '';
 
-		protected function renderFilters():string
-		{
-			if (empty ($this->taxonomies)) {
-				return '';
-			}
-			$inside = implode('', array_filter(array_map(function($tax) {
-				$registrar = Registrar::getInstance($tax);
-				if (!$registrar) return '';
+			$current = BASE.$this->content[0];
+			$contentFor = $registrar->registrar->for;
+			$hidden = in_array($current, $contentFor) ? '' : ' hidden';
 
-				$current = BASE.$this->content[0];
-				$contentFor = $registrar->registrar->for;
-				$hidden = in_array($current, $contentFor) ? '' : ' hidden';
+			$selector = new TaxonomySelector(
+				'feed-'.$tax,
+				$tax,
+				[
+					'icon'	=> $registrar->getIcon(),
+					'update'=> '.selected-items-section .selected-items',
+					'types'	=> $contentFor,
+					'autocomplete'	=> false,
+					'hidden'	=> $hidden,
+					'output'	=> 'minimal',
+					'search'	=> true
+				]
+			);
+			return $selector->render();
 
-				$selector = new TaxonomySelector(
-					'feed-'.$tax,
-					$tax,
-					[
-						'icon'	=> $registrar->getIcon(),
-						'update'=> '.selected-items-section .selected-items',
-						'types'	=> $contentFor,
-						'autocomplete'	=> false,
-						'hidden'	=> $hidden,
-						'output'	=> 'minimal',
-						'search'	=> true
-					]
-				);
-				return $selector->render();
-
-			}, $this->taxonomies)));
-			return sprintf(
-				'<div class="taxonomies row left">
+		}, $this->taxonomies)));
+		return sprintf(
+			'<div class="taxonomies row left">
 				<div class="row top wrap">
 					<span class="label">Filter By:</span>
 					%s
@@ -313,157 +372,157 @@
 					</div>
 				</div>
 			</div>',
-				$inside,
-				str_replace('class="toggle-text"', 'class="toggle-text" hidden', jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match'])),
-				jvbIcon('x')
-			);
-		}
+			$inside,
+			str_replace('class="toggle-text"', 'class="toggle-text" hidden', jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match'])),
+			jvbIcon('x')
+		);
+	}
 
-		protected function renderOrderControls():string
-		{
-			$orderby = [
-				[
-					'slug'	=> 'title',
-					'icon'	=> 'alphabetical',
-					'label'	=> 'Name'
-				],
-				[
-					'slug'	=> 'date',
-					'icon'	=> 'calendar',
-					'label'	=> 'Date Created',
-				],
-				[
-					'slug'	=> 'date_modified',
-					'icon'	=> 'clock-clockwise',
-					'label'	=> 'Date Modified'
-				]
-			];
-			$custom = $this->getCustomOrdering();
-			$orderby = $orderby + $custom;
-			$orderby[] = [
-				'slug'	=> 'random',
-				'icon'	=> 'shuffle',
-				'label'	=> 'Randomly'
-			];
+	protected function renderOrderControls():string
+	{
+		$orderby = [
+			[
+				'slug'	=> 'title',
+				'icon'	=> 'alphabetical',
+				'label'	=> 'Name'
+			],
+			[
+				'slug'	=> 'date',
+				'icon'	=> 'calendar',
+				'label'	=> 'Date Created',
+			],
+			[
+				'slug'	=> 'date_modified',
+				'icon'	=> 'clock-clockwise',
+				'label'	=> 'Date Modified'
+			]
+		];
+		$custom = $this->getCustomOrdering();
+		$orderby = $orderby + $custom;
+		$orderby[] = [
+			'slug'	=> 'random',
+			'icon'	=> 'shuffle',
+			'label'	=> 'Randomly'
+		];
 
-			$custom = implode(',', array_map(function($ord) {
-				return $ord['slug'];
-			}, $custom));
+		$custom = implode(',', array_map(function($ord) {
+			return $ord['slug'];
+		}, $custom));
 
-			$i = 0;
-			$orderby = sprintf(
-				'<div class="orderby row left">
+		$i = 0;
+		$orderby = sprintf(
+			'<div class="orderby row left">
 				<span class="label">Order by:</span>%s
 				</div>',
-				implode('', array_map(function ($by) use (&$i){
-					$checked = $i === 0 ? ' checked' : '';
-					$i++;
-					return sprintf(
-						'<input type="radio" id="order-%s" class="btn" name="orderby" value="%s" data-filter="orderby"%s%s>
+			implode('', array_map(function ($by) use (&$i){
+				$checked = $i === 0 ? ' checked' : '';
+				$i++;
+				return sprintf(
+					'<input type="radio" id="order-%s" class="btn" name="orderby" value="%s" data-filter="orderby"%s%s>
 							<label for="order-%s" title="Order %s">%s<span class="label">%s</span></label>',
-						$by['slug'],
-						$by['slug'],
-						$checked,
-						empty($by['for']??[]) ? '' : ' data-for="'.implode($by['for']).'"',
-						$by['slug'],
-						$by['slug'] === 'random' ? $by['label'] : 'by '.$by['label'],
-						jvbIcon($by['icon']),
-						$by['label']
-					);
-				}, $orderby))
-			);
+					$by['slug'],
+					$by['slug'],
+					$checked,
+					empty($by['for']??[]) ? '' : ' data-for="'.implode($by['for']).'"',
+					$by['slug'],
+					$by['slug'] === 'random' ? $by['label'] : 'by '.$by['label'],
+					jvbIcon($by['icon']),
+					$by['label']
+				);
+			}, $orderby))
+		);
 
-			$order = [
-				[
-					'slug'	=> 'desc',
-					'icon'	=> 'sort-descending',
-					'label'	=> 'Descending (A-Z, 1-10)'
-				],
-				[
-					'slug'	=> 'asc',
-					'icon'	=> 'sort-ascending',
-					'label'	=> 'Ascending (Z-A, 10-1)'
-				]
-			];
+		$order = [
+			[
+				'slug'	=> 'desc',
+				'icon'	=> 'sort-descending',
+				'label'	=> 'Descending (A-Z, 1-10)'
+			],
+			[
+				'slug'	=> 'asc',
+				'icon'	=> 'sort-ascending',
+				'label'	=> 'Ascending (Z-A, 10-1)'
+			]
+		];
 
-			$i = 0;
-			$order = sprintf(
-				'<div class="order-direction row left" data-for-order="date,date_modified,title%s">
+		$i = 0;
+		$order = sprintf(
+			'<div class="order-direction row left" data-for-order="date,date_modified,title%s">
 				<span class="label">Order:</span>
 				%s
 				</div>',
-				$custom === '' ? '' : ','.$custom,
-				implode('', array_map(function ($ord) use (&$i) {
-					$checked = $i=== 0 ? ' checked' : '';
-					$i++;
-					return sprintf(
-						'<input type="radio" id="order-%s" class="btn" name="order" value="%s" data-filter="order"%s>
+			$custom === '' ? '' : ','.$custom,
+			implode('', array_map(function ($ord) use (&$i) {
+				$checked = $i=== 0 ? ' checked' : '';
+				$i++;
+				return sprintf(
+					'<input type="radio" id="order-%s" class="btn" name="order" value="%s" data-filter="order"%s>
 						<label for="order-%s" title="Sort %s">
 							%s
 							<span class="label">%s</span>
 						</label>',
-						$ord['slug'],
-						$ord['slug'],
-						$checked,
-						$ord['slug'],
-						$ord['label'],
-						jvbIcon($ord['icon']),
-						$ord['label']
-					);
-				}, $order))
-			);
+					$ord['slug'],
+					$ord['slug'],
+					$checked,
+					$ord['slug'],
+					$ord['label'],
+					jvbIcon($ord['icon']),
+					$ord['label']
+				);
+			}, $order))
+		);
 
-			return sprintf(
-				'<div class="ordering row top left nowrap">%s%s</div>',
-				$orderby,
-				$order
-			);
-		}
-			protected function getCustomOrdering():array
-			{
-				$custom = [];
-				foreach ($this->content as $content) {
-					$registrar = Registrar::getInstance($content);
-					if (!$registrar || empty($registrar->config('feed')->getCustomOrder())) {
-						continue;
-					}
-					$custom = array_merge_recursive($custom, $registrar->config('feed')->getCustomOrder());
-				}
-				return $custom;
+		return sprintf(
+			'<div class="ordering row top left nowrap">%s%s</div>',
+			$orderby,
+			$order
+		);
+	}
+	protected function getCustomOrdering():array
+	{
+		$custom = [];
+		foreach ($this->content as $content) {
+			$registrar = Registrar::getInstance($content);
+			if (!$registrar || empty($registrar->config('feed')->getCustomOrder())) {
+				continue;
 			}
-		protected function renderViewControls():string
-		{
-			$views = [
-				'grid'	=> ['slug' => 'grid', 'icon' => 'squares-four', 'label' => 'Grid View'],
-				'list'	=> ['slug' => 'list', 'icon' => 'rows', 'label' => 'List View']
-			];
+			$custom = array_merge_recursive($custom, $registrar->config('feed')->getCustomOrder());
+		}
+		return $custom;
+	}
+	protected function renderViewControls():string
+	{
+		$views = [
+			'grid'	=> ['slug' => 'grid', 'icon' => 'squares-four', 'label' => 'Grid View'],
+			'list'	=> ['slug' => 'list', 'icon' => 'rows', 'label' => 'List View']
+		];
 
-			$i = 0;
-			return sprintf(
-				'<div class="view row left nowrap"><span class="label">Switch View:</span>%s</div>',
-				implode('', array_map(function ($view) use (&$i) {
-					$checked = $i === 0 ? ' checked' : '';
-					$i++;
-					return sprintf(
-						'<input type="radio"
+		$i = 0;
+		return sprintf(
+			'<div class="view row left nowrap"><span class="label">Switch View:</span>%s</div>',
+			implode('', array_map(function ($view) use (&$i) {
+				$checked = $i === 0 ? ' checked' : '';
+				$i++;
+				return sprintf(
+					'<input type="radio"
 						data-view="%s" value="%s" class="btn" name="view" id="view-%s"%s>
 						<label for="view-%s" title="%s">
 						%s<span class="label">%s</span>
 </label>',
-						$view['slug'],
-						$view['slug'],
-						$view['slug'],
-						$checked,
-						$view['slug'],
-						$view['label'],
-						jvbIcon($view['icon']),
-						$view['label'],
-					);
-				}, $views))
-			);
-		}
+					$view['slug'],
+					$view['slug'],
+					$view['slug'],
+					$checked,
+					$view['slug'],
+					$view['label'],
+					jvbIcon($view['icon']),
+					$view['label'],
+				);
+			}, $views))
+		);
+	}
 
-	protected function renderGrid():string
+	protected function renderPlaceholders():string
 	{
 		$placeholders = '';
 		$total = count($this->content) - 1;
@@ -492,13 +551,58 @@
 		);
 	}
 
+
+	protected function renderGrid():string
+	{
+		if (empty($this->args)) {
+			return $this->renderPlaceholders();
+		}
+		$items = $this->getItems();
+
+		$out = '<div class="item-grid">';
+		$out .= implode('',array_map(function ($ID) {
+			$content = $this->isContentTax
+				? jvbNoBase($this->content[0])
+				: jvbNoBase(get_post_type($ID));
+
+			return $this->renderItem($content, $ID);
+		}, $items));
+		$out .= '</div>';
+		return $out;
+	}
+	protected function getItems():array
+	{
+		if ($this->isContentTax) {
+			$items = get_terms([
+				'taxonomy'	=> $this->content,
+				'fields'	=> 'ids',
+				'meta_key'	=> BASE.'date_modified',
+				'meta_type'	=> 'DATETIME',
+				'orderby'	=> 'meta_value',
+				'order'		=> 'desc',
+				'number'	=> $this->args['posts_per_page']
+			]);
+			$items = $items && !is_wp_error($items) ? $items : [];
+		} else {
+			$items = new WP_Query($this->args);
+			$this->hasMore = $items->found_posts > $this->args['posts_per_page'];
+			$items = $items->posts;
+			wp_reset_postdata();
+		}
+		return $items;
+	}
+
 	protected function renderLoader():string
 	{
-		return sprintf(
-			'<button type="button" class="load-more">%s<span>Show Me More</span>%s</button>
-			%s%s',
+		$button = sprintf(
+			'<button type="button" class="load-more"%s>%s<span>Show Me More</span>%s</button>',
+			$this->hasMore ? '' : ' hidden',
 			jvbIcon('arrow-elbow-left-down'),
 			jvbIcon('arrow-elbow-right-down'),
+		);
+		return sprintf(
+			'%s%s%s',
+			$button,
 			jvbLoadingScreen(),
 			$this->isGallery ? jvbRenderGallery(false) : '',
 		);
@@ -542,10 +646,10 @@
 
 	protected function renderActions():string
 	{
-		return sprintf(
+		return is_user_logged_in() ? sprintf(
 			'<button data-action="refresh" data-ignore>%s<span>Hard Refresh</span></span></button>',
 			jvbIcon('arrows-clockwise')
-		);
+		) : '';
 	}
 
 	public static function getFavouritesButton(string $content):string
@@ -554,10 +658,11 @@
 		if (!$registrar || !Site::has('favourites') || !$registrar->hasFeature('favouritable')) {
 			return '';
 		}
-		return '<button class="favourite" type="button" title="Add to favourites" data-action="favourite">
-			'.jvbIcon('heart')
-			.jvbIcon('heart', ['style'=>'fill']).'
-		</button>';
+		return sprintf(
+			'<button class="favourite" type="button" title="Add to favourites" data-action="favourite">%s%s</button>',
+			jvbIcon('heart'),
+			jvbIcon('heart', ['style'=>'fill'])
+		);
 	}
 	public static function getUpvotesButton(string $content):string
 	{
@@ -565,98 +670,239 @@
 		if (!Site::has('karma') || !$registrar || !$registrar->hasFeature('karma')){
 			return '';
 		}
-		return '<div class="karma row">
+		return sprintf(
+			'<div class="karma row">
 			<button type="button" class="vote" data-action="upvote">
-				'.jvbIcon('arrow-fat-up')
-				.jvbIcon('arrow-fat-up', ['style'=>'fill']).
-			'</button>
+				%s%s
+			</button>
 			<button type="button" class="vote" data-action="downvote">
-				'.jvbIcon('arrow-fat-down')
-				.jvbIcon('arrow-fat-down', ['style'=>'fill']).
-			'</button>
+				%s%s
+			</button>
 			<span class="score"></span>
-		</div>';
+		</div>',
+			jvbIcon('arrow-fat-up'),
+			jvbIcon('arrow-fat-up', ['style'=>'fill']),
+			jvbIcon('arrow-fat-down'),
+			jvbIcon('arrow-fat-down', ['style'=>'fill'])
+		);
 	}
 	protected function getDefaultTemplate(string $content): string
 	{
-		$template = apply_filters('jvbFeedItem', '', $content);
-		if (empty($template)) {
-			$config = Registrar::getInstance($content)->getConfig('feed');
-			$allFields = Registrar::getFieldsFor($content);
-			$images = $config['images']??['post_thumbnail'];
-			$fields = $config['fields']??['post_title','post_date','post_excerpt'];
-			$fields = array_filter($fields, function($field) use($images) {
-				return !in_array($field, $images);
-			});
-			$fields = array_filter($allFields, function($field) use($fields) {
-				return in_array($field, $fields);
-			}, ARRAY_FILTER_USE_KEY);
-
-
-			$template = sprintf(
-				'<div class="feed item col %s">%s%s',
-				$content,
-				self::getFavouritesButton($content),
-				self::getUpvotesButton($content)
-			);
-
-			//Add all defined images, but allow for filtering
-			$imageTemplate = '<a>';
-			foreach ($images as $image) {
-				$imageTemplate .= sprintf(
-					'<img data-field="%s" width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
-					$image
-				);
-			}
-			$imageTemplate .= '</a>';
-
-			$template .= sprintf(
-				'<div class="images">%s</div>',
-				apply_filters('jvbFeedImages', $imageTemplate, $content, $images)
-			);
-
-			//Output default fields, but allow for filtering
-			$template .= sprintf(
-				'<details>
-		<summary>%s</summary>',
-				apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content)
-			);
-
-			$fieldsTemplate = '';
-			foreach ($fields as $fieldName => $config) {
-				$fieldsTemplate .= apply_filters('jvbFeedItemField', $this->defaultFieldTemplate($config['type'], $fieldName), $content, $fieldName, $config['type']);
-			}
-			$template .= sprintf(
-				'<div class="item-info">%s</div>',
-				apply_filters('jvbFeedItemFields', $fieldsTemplate, $content, $fields)
-			);
-			$template .= '</details></div>';
-		}
-
-
 		return sprintf(
 			'<template class="feedItem%s">%s</template>',
 			ucfirst($content),
-			$template
+			$this->renderItem($content)
 		);
 	}
 
-	protected function defaultFieldTemplate(string $fieldType, string $fieldName):string
+	protected function renderItem(string $content, ?int $ID = null):string
+	{
+		/**
+		 * Allow plugins to replace the content within the feed item
+		 */
+		$function = BASE.'render_'.$content.'_feed_item';
+		if (function_exists($function)) {
+			$out = $function($ID);
+		} else {
+			$out = $this->buildFeedItem($content, $ID);
+		}
+		$registrar = Registrar::getInstance($content);
+		$data = [];
+		if ($registrar && $registrar->hasFeature('is_timeline')) {
+			$data[] = 'data-timeline';
+		}
+
+		$data = !empty($data) ? ' '.implode(' ', $data) : '';
+
+		return sprintf(
+			'<div class="feed item col %s"%s>%s%s%s</div>',
+			$content,
+			$data,
+			self::getFavouritesButton($content),
+			self::getUpvotesButton($content),
+			$out
+		);
+	}
+	protected function buildFeedItem(string $content, ?int $ID = null):string
+	{
+		$registrar = Registrar::getInstance($content);
+		$meta = is_null($ID) ? false :
+			match ($registrar->getType()) {
+				'post'	=> Meta::forPost($ID),
+				'term'	=> Meta::forTerm($ID),
+				'user'	=> Meta::forUser($ID),
+				default => false
+			};
+
+		[$images, $fields] = $registrar->getFeedFields();
+
+		/**
+		 * Get the main image for the feed item.
+		 * Output can be overridden with the $imagesFn
+		 */
+		$imagesFn = BASE.'render_'.$content.'_feed_item_images';
+		if (function_exists($imagesFn)) {
+			$img = $imagesFn($ID, $images);
+		} else {
+			$img = '';
+			foreach ($images as $config) {
+				$field = $config['name'];
+				$img .= $meta ? jvbFormatImage($meta->get($field), 'tiny', 'medium')
+					: $this->defaultFieldTemplate($config['type'], $field);
+			}
+		}
+		$img = sprintf(
+			'<div class="images"><a href="%s">%s</a></div>',
+			$ID ? get_the_permalink($ID) : '',
+			$img
+		);
+
+		/**
+		 * Start the Details with the fields
+		 * Plugins can modify the summary title with the 'jvbFeedItemSummary' filter
+		 */
+		$summary = sprintf(
+			'<details><summary>%s</summary>',
+			apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content)
+		);
+		/**
+		 * Work through the fields
+		 * Each field output can be overridden with $field or $fieldType
+		 */
+		foreach ($fields as $config) {
+			$f = $config['name'];
+			$functions = [
+				BASE.'render_'.$content.'_field_'.$f, 	//Overrides field for this content
+				BASE.'render_field_'.$f,				//Overrides field with this name
+				BASE.'render_field_type_'.$config['type'] //Overrides field of this type
+			];
+
+			$didIt = false;
+			foreach ($functions as $func) {
+				if (!$didIt && function_exists($func)) {
+					$didIt = true;
+					$summary .= $func($ID, is_null($ID));
+				}
+			}
+			if (!$didIt) {
+				$summary .= $this->defaultFieldTemplate($config['type'], $f, is_null($ID), $meta, $config);
+			}
+
+		}
+		$summary .= '</details>';
+
+		return $img.$summary;
+	}
+
+	protected function defaultFieldTemplate(string $fieldType, string $fieldName, bool $isTemplate = true, bool|Meta $meta = false, array $config = []):string
 	{
 		$data = ' data-field="'.$fieldName.'"';
+		$value = $meta ? $meta->get($fieldName) : '';
+		if (!$isTemplate && empty($value)) {
+			return '';
+		}
 		switch ($fieldName) {
 			case 'post_title':
-				return '<h3'.$data.'></h3>';
+				return sprintf(
+					'<h3%s>%s</h3>',
+					$data,
+					$meta && !empty($value) ? $value : '',
+				);
 			case 'post_date':
 			case 'post_modified':
-				return '<time'.$data.'></time>';
+				return sprintf(
+					'<time%s%s>%s</time>',
+					$data,
+					$meta && !empty($value) ? date('c', strtotime($value)) : '',
+					$meta && !empty($value) ? date('F j, Y', strtotime($value)) : '',
+				);
 		}
-		return match($fieldType) {
-			'date','datetime','time' => '<time'.$data.'></time>',
-			'upload'	=> '<img'.$data.' width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
-			'taxonomy'	=> '<ul'.$data.'><li><a><i></i></a></li></ul>',
-			default	=>  '<p'.$data.'></p>',
+
+		return match ($fieldType) {
+			'date' => sprintf(
+				'<time%s%s>%s</time>',
+				$data,
+				$meta && !empty($value) ? date('c', strtotime($value)) : '',
+				$meta && !empty($value) ? date('F j, Y', strtotime($value)) : '',
+			),
+			'datetime' => sprintf(
+				'<time%s%s>%s</time>',
+				$data,
+				$meta && !empty($value) ? date('c', strtotime($value)) : '',
+				$meta && !empty($value) ? date('F j, Y at h:iA', strtotime($value)) : '',
+			),
+			'time' => sprintf(
+				'<time%s%s>%s</time>',
+				$data,
+				$value,
+				$value
+			),
+			'upload' => empty($value) ? sprintf(
+				'<img%s width="300px" height="300px" loading="lazy" decoding="async">',
+				$data
+			) :
+				str_replace('<img', '<img' . $data, jvbFormatImage($value)),
+			'selector' => sprintf(
+				'<ul%s class="terms %s">%s</ul>',
+				$data,
+				$config['taxonomy']??$config['post_type']??$config['role']??'',
+				empty($value) ? sprintf('<li><a>%s</a></li>',
+					$this->iconFor($config)
+				) :
+					implode('', array_filter(array_map(function ($ID) use ($config) {
+						$type = $config['subtype'];
+						$taxonomy = isset($config['taxonomy']) ? jvbCheckBase($config['taxonomy']) : '';
+
+						$item = match ($type) {
+							'taxonomy' => get_term($ID, $taxonomy),
+							'user' => get_userdata($ID),
+							'post' => get_post($ID),
+							default => false,
+						};
+
+						if (!$item || is_wp_error($item)) {
+							return '';
+						}
+
+						$icon = $this->iconFor($config);
+
+						$name = match ($type) {
+							'taxonomy' => $item->name,
+							'user' => $item->display_name,
+							'post' => $item->post_title,
+							default => '',
+						};
+						$url = match ($type) {
+							'taxonomy' => get_term_link((int)$ID, $taxonomy),
+							'user' => get_the_permalink(get_user_meta($ID, BASE . 'userLink', true)),
+							'post' => get_the_permalink($ID),
+							default => ''
+						};
+						return sprintf(
+							'<li><a href="%s">%s%s</a></li>',
+							$url,
+							!empty($icon) ? jvbIcon($icon) : '',
+							$name
+						);
+					}, explode(',', $value))))
+			),
+			default => sprintf(
+				'<p%s>%s</p>',
+				$data,
+				$value
+			),
 		};
 	}
+	protected function iconFor(array $fieldConfig):string
+	{
+		$icon = match ($fieldConfig['type']??'') {
+			'taxonomy' => Registrar::getInstance($fieldConfig['taxonomy'])->getIcon(),
+			'user' => isset($fieldConfig['role']) ? Registrar::getInstance($fieldConfig['role'])->getIcon() : 'user',
+			'post' => Registrar::getInstance($fieldConfig['post_type'])->getIcon(),
+			default => ''
+		};
+
+		return empty($icon) ? $icon : jvbIcon($icon);
+	}
 
 }

--
Gitblit v1.10.0