From 47e77f9fac1155c536b2b87fec552c7fcce66fa6 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 01 Jun 2026 18:06:34 +0000
Subject: [PATCH] =Timeline block fixes. Next up: adding article schema classes

---
 inc/blocks/FeedBlock.php |  915 ++++++++++++++++++++++++++++++++++++++------------------
 1 files changed, 617 insertions(+), 298 deletions(-)

diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 6bbe0e4..3693d93 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -1,343 +1,662 @@
 <?php
 namespace JVBase\blocks;
 
-use JVBase\managers\CacheManager;
+use JVBase\managers\Cache;
+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; // Exit if accessed directly
+	exit;
 }
 
 class FeedBlock
 {
-    protected CacheManager $cache;
-    protected array $config;
-    protected string $path = JVB_DIR.'/build/feed';
+	protected Cache $cache;
+	protected array $config;
+	protected string $path = JVB_DIR.'/build/feed';
 
-    public function __construct()
-    {
-        $this->cache = new CacheManager('feed', WEEK_IN_SECONDS);
-        add_action('init', [$this, 'registerBlock']);
-    }
+	protected array $content = [];
+	protected array $taxonomies = [];
+	protected ?string $context = null;
+	protected ?int $contextID = null;
 
-    public function registerBlock()
-    {
-        register_block_type($this->path, [
-            'render_callback' => [$this, 'render']
-        ]);
-    }
+	protected bool $isGallery = false;
 
-    protected function buildParams(array $attributes):array
-    {
+	public function __construct()
+	{
+		// Initialize cache with connections
+		$this->cache = Cache::for('feed_block', WEEK_IN_SECONDS);
+		if (JVB_TESTING) {
+			$this->cache->flush();
+		}
 
-        if (!jvbCheck('inheritQuery', $attributes)) {
-            return [
-                'title'     => $attributes['title'],
-                'content'   => $attributes['contentTypes'],
-                'taxonomies'=> $this->getTaxonomies($attributes['contentTypes'])
-            ];
-        }
+		add_action('init', [$this, 'registerBlock']);
+	}
 
-        if (isJVBUserType()) {
-            return $this->buildProfileConfig();
-        } elseif (isJVBContentTax()) {
-            return $this->buildShopConfig();
-        } elseif (is_tax()) {
-            return $this->buildTaxonomyConfig();
-        }
+	public function registerBlock()
+	{
+		register_block_type($this->path, [
+			'render_callback' => [$this, 'render']
+		]);
+	}
 
-        return [];
-    }
+	public function render(array $attributes): string
+	{
+		if (is_post_type_archive(BASE.'directory')) {
+			return '';
+		}
+		$this->determineContent($attributes);
+		if (empty($this->content)) {
+			return '';
+		}
+		$this->determineTaxonomies();
+		$classes = '';
 
-    protected function buildProfileConfig():array
-    {
-        $obj = get_queried_object();
-        $content = jvbGetUserContentTypes($obj->ID);
-        return [
-            'is_gallery'    => true,
-            'content'       => $content,
-            'context'       => jvbNoBase($obj->post_type),
-            'taxonomies'    => $this->getTaxonomies($content)
-        ];
-    }
-
-    protected function buildShopConfig():array
-    {
-        $type = jvbNoBase(get_queried_object()->taxonomy);
-        $content = jvbContentTaxContent($type);
-
-        $context = [
-            'content'   => $content,
-            'taxonomies' => $this->getTaxonomies($content),
-            'context'   => $type,
-        ];
-        unset($context['taxonomies'][array_search($type, $context['taxonomies'])]);
-        return $context;
-    }
-
-    protected function buildTaxonomyConfig():array
-    {
-        $type = jvbNoBase(get_queried_object()->taxonomy);
-        $content = jvbContentTaxContent($type);
-        return [
-            'content'   => $content,
-            'taxonomies' => $this->getTaxonomies($content),
-            'context'   => $type
-        ];
-    }
-
-    protected function getTaxonomies(array $content):array
-    {
-        global $jvb_taxonomy_for;
-        $taxonomies = [];
-        foreach ($jvb_taxonomy_for as $taxonomy => $for) {
-            if (array_intersect($for, $content)) {
-                $taxonomies[] = $taxonomy;
-            }
-        }
-        return $taxonomies;
-    }
-
-    public function render(array $attributes, string $content, WP_Block $block)
-    {
-        $this->config = $this->buildParams($attributes);
-
-		return $this->cache->remember(
-			$this->config,
-			function() {
-				return $this->renderBlock();
-			}
+		return sprintf(
+			'<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(),
+			$this->renderLoader(),
+			TaxonomySelector::outputSelectorModal(),
+			$this->renderActions()
 		);
-    }
+	}
 
-    protected function renderBlock():string
-    {
-        $work = isJVBUserType() ? ' id="work"' : '';
+	protected function determineContent(array $attrs):void
+	{
+		if (array_key_exists('inheritQuery', $attrs) && $attrs['inheritQuery'] === true) {
+			if (is_post_type_archive()) {
+				$obj = get_queried_object();
+				$this->content = [jvbNoBase($obj->name)];
+				return;
+			} elseif (!empty(Registrar::getProfileTypes()) && is_singular(Registrar::getProfileTypes())) {
+				global $post;
+				$author = $post->post_author;
+				$role = jvbUserRole($author);
 
-        ob_start();
-        ?>
-        <section<?= $work ?> class="feed-block"
-                             data-source="<?= get_queried_object_id(); ?>"
-            <?= (array_key_exists('context', $this->config)) ? ' data-context="'.$this->config['context'].'"' : '' ?>
-            <?= (array_key_exists('is_gallery', $this->config)) ? ' data-gallery="true"' : ''?>>
-            <?php
-            $this->renderFilters();
-            $this->renderGrid();
-            $this->renderLoader();
-            $this->renderTemplates();
-            ?>
-        </section>
-        <?php
-        return ob_get_clean();
-    }
+				$this->context = jvbNoBase($role);
+				$this->contextID = $author;
+				$registrar = Registrar::getInstance($role);
+				if (!$registrar) {
+					return;
+				}
+				$this->content = $registrar->getCreatable();
+				return;
+			} elseif (is_tax()) {
+				$obj = get_queried_object();
+				$this->context = jvbNoBase($obj->taxonomy);
+				$this->contextID = $obj->term_id;
+				$registrar = Registrar::getInstance($obj->taxonomy);
+				if (!$registrar) {
+					return;
+				}
+				if ($registrar->hasFeature('is_content')) {
+					//example: tattoo shop, etc TODO
+					return;
+				}
+				$this->content = array_map(function ($item) { return jvbNoBase($item); }, $registrar->registrar->for);
+				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) {
 
-    protected function renderFilters():void
-    {
-        if (empty($this->config)) {
-            return;
-        }
-        $many = count($this->config['content']) > 1;
-        global $jvb_everything;
-        global $jvb_taxonomy_for;
-        ?>
-        <form class="feed-filters" data-save="feed">
-            <details>
-            <summary class="row btw">
-                <span class="label">SHOWING: </span>
-                <?php
-                $labels = [];
-                foreach ($this->config['content'] as $i => $type) :
-                    $checked = $i === 0 ? ' checked' : '';
+				$registrar = Registrar::getInstance($content);
+				if (!$registrar) continue;
+				$theTax = $registrar->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;
+						}
+					}
+				}
+			}
+			$this->taxonomies = array_unique($taxonomies);
+		}
 
-                    $label = $jvb_everything[$type]['plural'];
-                    ?>
+	protected function renderFiltersAndControls():string
+	{
+		return sprintf(
+			'<details class="all-filters col top left" data-ignore>
+			<summary>Filters %s</summary>
+			%s%s%s%s%s
+			</details>
+			<button data-action="clear-filters" data-ignore hidden>%s<span>Clear Filters</span></span></button>',
+			$this->renderContentLabels(),
+			$this->renderSearch(),
+			$this->renderContent(),
+			$this->renderFilters(),
+			$this->renderOrderControls(),
+			$this->renderViewControls(),
+			jvbIcon('x')
+		);
+	}
+		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]??''
+			);
 
-                    <input type="radio"
-                           id="filter-<?= esc_attr($type) ?>"
-                           class="btn"
-                           name="content"
-                           value="<?= esc_attr($type) ?>"
-                        <?= $checked ?>>
-                    <label for="filter-<?= esc_attr($type) ?>" title="Show <?= $label ?>" class="row">
-                        <?= jvbIcon($type, ['title'=> $label]) ?>
-                        <span class="screen-reader-text"><?= $label ?></span>
-                    </label>
+		}
+		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(
+					'<div class="content row right">
+						<input type="hidden" name="content" value="%s">
+						%s
+						</div>',
+					implode(',', $this->content),
+					$favourites
+				);
+			}
+			$i = 0;
+			$content = implode('', array_map(function($type) use (&$i) {
+				$registrar = Registrar::getInstance($type);
 
-                <?php
-                $labels['filter-'.$type] = $label;
-                endforeach;
-                ?>
-                <ul class="filter-label">
-                <?php
-                $i = 0;
-                foreach ($labels as $id =>$label) {
-                    $active = $i === 0 ? ' class="active"' : '';
-                    ?>
-                    <li id="<?=$id?>"<?=$active?>>
-                        <?=$label?>
-                    </li>
-                    <?php
-                    $i++;
-                }
-                ?>
-                </ul>
+				$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"
+					id="filter-%s"
+					class="btn"
+					name="content"
+					data-filter="content"
+					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));
 
 
-                <?php if (is_user_logged_in()) : ?>
-                    <input type="checkbox" id="favourites" class="btn" name="favourites" value="on">
-                    <label for="favourites" title="Show Favourites" class="row">
-                        <?= jvbIcon('heart', ['title'    =>'Favourites']) ?>
-                        <span class="screen-reader-text">Show Favourites Only</span>
-                    </label>
-                <?php endif; ?>
 
-                <?php if ($many) {
-                    echo '</summary>';
-                } ?>
+			return sprintf(
+				'<div class="content row left nowrap"><span class="label">Showing:</span>
+					%s%s
+				</div>',
+				$content,
+				$favourites
+			);
+		}
 
-                <div class="filters">
-                    <div class="filter-group">
-                        <span class="label">FILTER BY:</span>
+		protected function renderFilters():string
+		{
+			if (empty ($this->taxonomies)) {
+				return '';
+			}
+			$inside = implode('', array_filter(array_map(function($tax) {
+				$registrar = Registrar::getInstance($tax);
+				if (!$registrar) return '';
 
-                        <?php
-                        foreach ($jvb_taxonomy_for as $tax => $items) :
-                            $hidden = !in_array($tax, $this->config['taxonomies']) ? ' hidden' : '';
-                            if (in_array($tax, $this->config['taxonomies'])) {
-                                $tax = new TaxonomySelector(
-                                    'feed-'.$tax,
-                                    $tax,
-                                    [
-										'update' => '.selected-items-section .selected-items',
-                                        'types' => $items,
-										'hidden'=> $hidden
-                                    ]
-                                );
-                                echo  $tax->render();
-                            }
+				$current = BASE.$this->content[0];
+				$contentFor = $registrar->registrar->for;
+				$hidden = in_array($current, $contentFor) ? '' : ' hidden';
 
-                        endforeach; ?>
-                    </div>
-                    <div class="selected-items-section">
-                        <div class="selected-items row"></div>
-                        <div class="filter-actions row">
-                            <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY') ?>
-                            <button type="button" class="clear-filters row">
-                                <?= jvbIcon('close', ['title'    => 'Clear']) ?>
-                                Clear All Filters
-                            </button>
-                        </div>
-                    </div>
-                </div>
+				$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();
 
-                <div class="filter-group">
-                    <div class="order-by">
-                        <span class="label">ORDER BY:</span>
-                        <input type="radio" id="order-title" class="btn" name="orderby" value="title" data-for="artist,shop" hidden>
-                        <label for="order-title" title="Order by Name" class="row">
-                            <?= jvbIcon('alphabetical') ?>
-                            <span class="label">Name</span>
-                        </label>
+			}, $this->taxonomies)));
+			return sprintf(
+				'<div class="taxonomies row left">
+				<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>
+					<div class="filter-actions row">
+						%s
+						<button type="button" class="clear-filters" hidden>
+							%s
+							<span>Clear All Filters</span>
+						</button>
+					</div>
+				</div>
+			</div>',
+				$inside,
+				str_replace('class="toggle-text"', 'class="toggle-text" hidden', jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match'])),
+				jvbIcon('x')
+			);
+		}
 
-                        <input type="radio" id="order-date" class="btn" name="orderby" value="date" checked>
-                        <label for="order-date" title="Order by Date" class="row">
-                            <?= jvbIcon('calendar', ['title'=>'Date']) ?>
-                            <span class="label">Date</span>
-                        </label>
+		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'
+			];
 
-                        <input type="radio" id="order-random" class="btn" name="orderby" value="random">
-                        <label for="order-random" title="Random Order" class="row">
-                            <?= jvbIcon('random') ?>
-                            <span class="label">Random</span>
-                        </label>
-                    </div>
+			$custom = implode(',', array_map(function($ord) {
+				return $ord['slug'];
+			}, $custom));
 
-                    <div class="order-direction radio-group-label" data-for-order="date,title">
-                        <span class="label">ORDER:</span>
-                        <input type="radio" id="order-desc" class="btn" name="order" value="desc" checked>
-                        <label for="order-desc" title="Newest First" class="row">
-                            <?= jvbIcon('desc') ?>
-                        </label>
+			$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>
+							<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))
+			);
 
-                        <input type="radio" id="order-asc" class="btn" name="order" value="asc">
-                        <label for="order-asc" title="Oldest First" class="row">
-                            <?= jvbIcon('asc') ?>
-                        </label>
-                    </div>
-                </div>
-            </details>
-        </form>
-        <?php
-    }
+			$order = [
+				[
+					'slug'	=> 'desc',
+					'icon'	=> 'sort-descending',
+					'label'	=> 'Descending (A-Z, 1-10)'
+				],
+				[
+					'slug'	=> 'asc',
+					'icon'	=> 'sort-ascending',
+					'label'	=> 'Ascending (Z-A, 10-1)'
+				]
+			];
 
-    protected function renderGrid():void
-    {
-        ?>
-        <div class="item-grid"></div>
-        <?php
-    }
+			$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>
+						<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))
+			);
 
-    protected function renderLoader():void
-    {
-        ?>
-        <button type="button" class="load-more">
-            <?= jvbIcon('elbow-left-down', ['title'    =>'More']) ?>
-            Show Me More
-            <?= jvbIcon('elbow-right-down', ['title'=> 'More']) ?>
-        </button>
+			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;
+			}
+		protected function renderViewControls():string
+		{
+			$views = [
+				'grid'	=> ['slug' => 'grid', 'icon' => 'squares-four', 'label' => 'Grid View'],
+				'list'	=> ['slug' => 'list', 'icon' => 'rows', 'label' => 'List View']
+			];
 
-        <?= jvbLoadingScreen() ?>
-        <?php
-        if (array_key_exists('is_gallery', $this->config)) {
-            jvbRenderGallery();
-        }
-    }
+			$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))
+			);
+		}
 
-    protected function renderTemplates():void
-    {
-        echo '<template class="feed-item">
-        <details class="item feed" data-umami-event="view_feed">
-            <summary class="row btw">
-                <span class="handle">DETAILS</span>
-                <button class="favourite" title="Add to favourites" onclick="toggleFavourite(this)">
-                    '.jvbIcon('heart')
-               .jvbIcon('heart', ['style'=>'fill']).'
-                </button>
-                <div class="feed-images">
-                    <a>
-                        <img width="300px" height="300px" loading="lazy" decoding="async">
-                    </a>
-                </div>
-            </summary>
+	protected function renderGrid():string
+	{
+		$placeholders = '';
+		$total = count($this->content) - 1;
+		$icons = [];
+		$icon = apply_filters('jvbFeedPlaceholder', '');
 
-            <div class="item-info">
-                <h3><a></a></h3>
-                <div class="item">
-                    <span class="label"></span>
-                    <a></a>
-                    <p></p>
-                </div>
-                <div class="item-list">
-                    <span class="label"></span>
-                        <ul>
-                            <li>
-                                <a></a>
-                            </li>
-                        </ul>
-                    </div>
-                </div>
-            </div>
-        </details>
-    </template>';
+		for ($i=1; $i<=36; $i++) {
+			if (empty($icon)) {
+				$rand = $total === 0 ? $total : rand(0, $total);
+				$content = $this->content[$rand];
+				if (!in_array($content, $icons)) {
+					$icons[$content] = Registrar::getInstance($content)->getIcon();
+				}
+				$icon = jvbIcon($icons[$content]);
+			}
 
-        echo '<template class="emptyState">
-            <div class="feed-empty-state">
-                <h3>NOTHING HERE...</h3>
+			$placeholders .= sprintf(
+				'<div class="placeholder">%s</div>',
+				$icon
+			);
+		}
+
+		return sprintf(
+			'<div class="item-grid">%s</div>',
+			$placeholders
+		);
+	}
+
+	protected function renderLoader():string
+	{
+		return sprintf(
+			'<button type="button" class="load-more">%s<span>Show Me More</span>%s</button>
+			%s%s',
+			jvbIcon('arrow-elbow-left-down'),
+			jvbIcon('arrow-elbow-right-down'),
+			jvbLoadingScreen(),
+			$this->isGallery ? jvbRenderGallery(false) : '',
+		);
+	}
+
+	protected function renderTemplates():string
+	{
+
+		$templates = [];
+		foreach ($this->content as $content) {
+			$templates[] =  $this->getDefaultTemplate($content);
+		}
+
+		$templates[] = sprintf(
+			'<template class="feedTerm"><button class="remove-term">%s<span></span>%s</button></template>',
+			jvbIcon(jvbDefaultIcon()),
+			jvbIcon('x')
+		);
+		$defaultEmptyState = sprintf(
+			'<div class="empty-state">
+                <h3>%sNOTHING HERE%s</h3>
                 <p>Try tweaking those filters a bit.</p>
-                <p>Edmonton\'s got talent - let\'s find it.</p>
-            </div>
-        </template>';
+            </div>',
+			jvbIcon(jvbDefaultIcon()),
+			jvbIcon(jvbDefaultIcon()),
+		);
+		$emptyState = apply_filters('jvbFeedEmptyState', $defaultEmptyState, $this->content);
+		$templates[] = sprintf(
+			'<template class="emptyState">%s</template>',
+			$emptyState
+		);
 
-        echo '<template class="placeholderTemplate"><div class="placeholder"></div></template>';
-    }
+		$placeholder = apply_filters('jvbFeedPlaceholder', jvbIcon(jvbLogoIcon()));
+		$templates[] = sprintf(
+			'<template class="placeholderTemplate"><div class="placeholder">%s</div></template>',
+			$placeholder
+		);
+
+		return implode('', $templates);
+	}
+
+	protected function renderActions():string
+	{
+		return sprintf(
+			'<button data-action="refresh" data-ignore>%s<span>Hard Refresh</span></span></button>',
+			jvbIcon('arrows-clockwise')
+		);
+	}
+
+	public static function getFavouritesButton(string $content):string
+	{
+		$registrar = Registrar::getInstance($content);
+		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>';
+	}
+	public static function getUpvotesButton(string $content):string
+	{
+		$registrar = Registrar::getInstance($content);
+		if (!Site::has('karma') || !$registrar || !$registrar->hasFeature('karma')){
+			return '';
+		}
+		return '<div class="karma row">
+			<button type="button" class="vote" data-action="upvote">
+				'.jvbIcon('arrow-fat-up')
+				.jvbIcon('arrow-fat-up', ['style'=>'fill']).
+			'</button>
+			<button type="button" class="vote" data-action="downvote">
+				'.jvbIcon('arrow-fat-down')
+				.jvbIcon('arrow-fat-down', ['style'=>'fill']).
+			'</button>
+			<span class="score"></span>
+		</div>';
+	}
+	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
+		);
+	}
+
+	protected function defaultFieldTemplate(string $fieldType, string $fieldName):string
+	{
+		$data = ' data-field="'.$fieldName.'"';
+		switch ($fieldName) {
+			case 'post_title':
+				return '<h3'.$data.'></h3>';
+			case 'post_date':
+			case 'post_modified':
+				return '<time'.$data.'></time>';
+		}
+		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>',
+		};
+	}
+
 }

--
Gitblit v1.10.0