From 235ce5716edc2f7cbe80fdccf26eac7269587839 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 08 Jun 2026 04:38:18 +0000
Subject: [PATCH] =FavouritesManager.php and FavouritesRoutes.php fixes. Moving all logic to FavouritesManager.php. Still some left to do

---
 inc/blocks/FeedBlock.php |  484 +++++++++++++++++++++++++++++++++++++++++------------
 1 files changed, 372 insertions(+), 112 deletions(-)

diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 3ed2f38..abe8395 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -2,11 +2,13 @@
 namespace JVBase\blocks;
 
 use JVBase\managers\Cache;
+use JVBase\meta\Meta;
 use JVBase\registrar\Registrar;
 use JVBase\base\Site;
 use JVBase\forms\TaxonomySelector;
 use JVBase\ui\CRUDSkeleton;
 use WP_Block;
+use WP_Query;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -20,7 +22,13 @@
 
 	protected array $content = [];
 	protected array $taxonomies = [];
+	protected ?string $context = null;
+	protected ?int $contextID = null;
+
 	protected bool $isGallery = false;
+	protected array $args;
+	protected bool $isContentTax = false;
+	protected bool $hasMore = false;
 
 	public function __construct()
 	{
@@ -29,6 +37,7 @@
 		if (JVB_TESTING) {
 			$this->cache->flush();
 		}
+		$this->cache->flush();
 
 		add_action('init', [$this, 'registerBlock']);
 	}
@@ -53,13 +62,15 @@
 		$classes = '';
 
 		return sprintf(
-			'<section class="feed-block%s" data-content="%s"%s>
+			'<section class="feed-block%s" data-content="%s"%s%s%s>
 			%s%s%s%s%s
 			<footer>%s</footer>
 			</section>',
 			$classes,
 			$this->getContent(),
 			$this->isGallery ? ' data-gallery' : '',
+			is_null($this->context) ? '' : ' data-context="'.$this->context.'"',
+			is_null($this->contextID) ? '' : ' data-context-id="'.$this->contextID.'"',
 			$this->renderFiltersAndControls(),
 			$this->renderGrid(),
 			$this->renderTemplates(),
@@ -72,36 +83,77 @@
 	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);
 				if (!$registrar) {
 					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']??[];
+
+		$args['post_type'] = array_map('jvbCheckBase', $attrs['contentTypes']);
+		$this->args = $args;
 	}
 		protected function getContent():string
 		{
@@ -133,7 +185,7 @@
 	protected function renderFiltersAndControls():string
 	{
 		return sprintf(
-			'<details class="all-filters col top left" data-ignore open>
+			'<details class="all-filters col top left" data-ignore>
 			<summary>Filters %s</summary>
 			%s%s%s%s%s
 			</details>
@@ -159,23 +211,11 @@
 		}
 		protected function renderContentLabels():string
 		{
-			$inside = '';
-			if (count($this->content) === 1) {
-				return '';
-			}
-			foreach ($this->content as $i => $type) {
-				$active = $i === 0 ? ' class="active"' : '';
-				$inside .= sprintf(
-					'<li id="filter-%s"%s>%s</li>',
-					$type,
-					$active,
-					Registrar::getInstance($type)->getPlural()??''
-				);
-			}
 			return sprintf(
-				'<ul class="filter-label">%s</ul>',
-				$inside
+				'<span class="label">Showing: <span class="current">%s</span></span>',
+				$this->content[0]??''
 			);
+
 		}
 		protected function renderContent():string
 		{
@@ -191,7 +231,7 @@
 			if (count($this->content) === 1) {
 				return empty($favourites)
 					? sprintf(
-						'<input type="hidden" name="content" value="%s">',
+						'<input type="hidden" data-filter="content" name="content" value="%s">',
 						implode(',', $this->content)
 					)
 					: sprintf(
@@ -205,23 +245,55 @@
 			}
 			$i = 0;
 			$content = implode('', array_map(function($type) use (&$i) {
-				$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;
+					}
+				}
+				$check = new WP_Query($args);
+				$hasPosts = !empty($check->posts);
+				wp_reset_postdata();
+
+				$disabled = !$hasPosts;
+				$checked = $i === 0 && $hasPosts;
+				if ($hasPosts) {
+					$i++;
+				}
 				return sprintf(
 					'<input type="radio"
 					id="filter-%s"
 					class="btn"
 					name="content"
 					data-filter="content"
-					value="%s"%s>
-					<label for="filter-%s" title="Show %s">%s<span class="screen-reader-text">%s</span></label>',
+					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,
-					$i === 0 ? ' checked' : '',
+					$checked ? ' checked' : '',
+					$disabled ? ' disabled' : '',
 					$type,
-					$registrar->getPlural(),
+					$registrar->getSingular(),
 					jvbIcon($registrar->getIcon()),
-					$registrar->getPlural()
+					$registrar->getSingular()
 				);
 			}, $this->content));
 
@@ -267,9 +339,9 @@
 			}, $this->taxonomies)));
 			return sprintf(
 				'<div class="taxonomies row left">
-				<div class="row top nowrap">
-				<span class="label">Filter By:</span>
-				<div class="row left">%s</div>
+				<div class="row top wrap">
+					<span class="label">Filter By:</span>
+					%s
 				</div>
 				<div class="selected-items-section">
 					<div class="selected-items row left"></div>
@@ -314,6 +386,7 @@
 				'icon'	=> 'shuffle',
 				'label'	=> 'Randomly'
 			];
+
 			$custom = implode(',', array_map(function($ord) {
 				return $ord['slug'];
 			}, $custom));
@@ -382,7 +455,7 @@
 			);
 
 			return sprintf(
-				'<div class="ordering row left nowrap">%s%s</div>',
+				'<div class="ordering row top left nowrap">%s%s</div>',
 				$orderby,
 				$order
 			);
@@ -431,7 +504,7 @@
 			);
 		}
 
-	protected function renderGrid():string
+	protected function renderPlaceholders():string
 	{
 		$placeholders = '';
 		$total = count($this->content) - 1;
@@ -460,13 +533,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) : '',
 		);
@@ -510,10 +628,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
@@ -522,10 +640,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
 	{
@@ -533,98 +652,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