Jake Vanderwerf
2025-11-23 d7dbe7fee362d587dfc334135d9581b6216a4295
inc/blocks/FeedBlock.php
@@ -2,306 +2,349 @@
namespace JVBase\blocks;
use JVBase\managers\CacheManager;
use JVBase\utility\Features;
use JVBase\utility\Checker;
use JVBase\forms\TaxonomySelector;
use WP_Block;
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 CacheManager $cache;
   protected array $config;
   protected string $path = JVB_DIR.'/build/feed';
    public function __construct()
    {
        $this->cache = CacheManager::for('feed',WEEK_IN_SECONDS);
        add_action('init', [$this, 'registerBlock']);
    }
   public function __construct()
   {
      // Initialize cache with connections
      $this->cache = CacheManager::for('feed_block', WEEK_IN_SECONDS);
    public function registerBlock()
    {
        register_block_type($this->path, [
            'render_callback' => [$this, 'render']
        ]);
    }
      // Set up cache connections for all feed content types
      $this->setupCacheConnections();
    protected function buildParams(array $attributes):array
    {
      add_action('init', [$this, 'registerBlock']);
   }
        if (!jvbCheck('inheritQuery', $attributes)) {
            return [
                'title'     => $attributes['title'],
                'content'   => $attributes['contentTypes'],
                'taxonomies'=> $this->getTaxonomies($attributes['contentTypes'])
            ];
        }
   /**
    * Set up cache connections for feed content
    */
   protected function setupCacheConnections(): void
   {
      // Connect to all content types that show in feed
      $contentTypes = Features::getTypesWithFeature('show_feed', 'content');
      foreach ($contentTypes as $type) {
         CacheManager::for('feed_content')->connectTo('post', $type);
      }
        if (isJVBUserType()) {
            return $this->buildProfileConfig();
        } elseif (isJVBContentTax()) {
            return $this->buildShopConfig();
        } elseif (is_tax()) {
            return $this->buildTaxonomyConfig();
        }
      // Connect to all taxonomies that show in feed
      $taxonomies = Features::getTypesWithFeature('show_feed', 'taxonomy');
      foreach ($taxonomies as $tax) {
         CacheManager::for('feed_taxonomy')->connectTo('taxonomy', $tax);
      }
   }
        return [];
    }
   public function registerBlock()
   {
      register_block_type($this->path, [
         'render_callback' => [$this, 'render']
      ]);
   }
    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 buildParams(array $attributes): array
   {
      if (!jvbCheck('inheritQuery', $attributes)) {
         return [
            'title' => $attributes['title'],
            'content' => $attributes['contentTypes'],
            'taxonomies' => $this->getTaxonomies($attributes['contentTypes'])
         ];
      }
      $config = [
         'is_gallery'    => false,
         'content'       => '',
         'taxonomies'    => []
      ];
      $type = get_queried_object();
    protected function buildShopConfig():array
    {
        $type = jvbNoBase(get_queried_object()->taxonomy);
        $content = jvbContentTaxContent($type);
      if (is_post_type_archive() || is_singular()) {
         $content = is_singular() ? jvbNoBase($type->post_type) : jvbNoBase($type->name);
         $mainConfig = JVB_CONTENT[$content]??false;
         if ($mainConfig && array_key_exists('feed', $mainConfig) && array_key_exists('config', $mainConfig['feed'])){
            $config = array_merge($config, $mainConfig['feed']['config']);
         } else {
            $config['content'] = $content;
            $config['icon'] = JVB_CONTENT[$content]['icon']??['logo-triangle'];
         }
         if (is_singular()) {
            $config['source'] = $type->ID;
         }
        $context = [
            'content'   => $content,
            'taxonomies' => $this->getTaxonomies($content),
            'context'   => $type,
        ];
        unset($context['taxonomies'][array_search($type, $context['taxonomies'])]);
        return $context;
    }
         $config['taxonomies'] = $this->getTaxonomies([$content]);
      } elseif (is_tax()) {
         $content = jvbNoBase($type->taxonomy);
         $mainConfig = JVB_TAXONOMY[$content]??false;
         if ($mainConfig) {
            $config['content'] = $mainConfig['for_content'];
            $config['context'] = $content;  // ← ADD THIS
            $config['taxonomies'] = $this->getTaxonomies($mainConfig['for_content']);
            if (array_key_exists('feed', $mainConfig) && array_key_exists('config', $mainConfig['feed'])){
               $config = array_merge($config, $mainConfig['feed']['config']);
            }
         }
         $config['source'] = $type->term_id;
      }
    protected function buildTaxonomyConfig():array
    {
        $type = jvbNoBase(get_queried_object()->taxonomy);
        $content = jvbContentTaxContent($type);
        return [
            'content'   => $content,
            'taxonomies' => $this->getTaxonomies($content),
            'context'   => $type
        ];
    }
      if (!is_array($config['content'])) {
         $config['content'] = [$config['content']];
      }
    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;
    }
      return $config;
   }
    public function render(array $attributes, string $content, WP_Block $block)
    {
        $this->config = $this->buildParams($attributes);
   /**
    * Get taxonomies for given content types
    * Uses Checker instead of globals
    */
   protected function getTaxonomies(array $content): array
   {
      $checker = Checker::getInstance();
      $taxonomies = [];
      foreach ($content as $contentType) {
         $contentTaxonomies = $checker->getTaxonomiesForContent($contentType);
         $contentTaxonomies = array_filter($contentTaxonomies, function($taxonomy) {
            return array_key_exists('show_feed', JVB_TAXONOMY[$taxonomy]) && JVB_TAXONOMY[$taxonomy]['show_feed'];
         });
         $taxonomies = array_merge($taxonomies, $contentTaxonomies);
      }
      return array_unique($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();
         }
      );
    }
   }
    protected function renderBlock():string
    {
        $work = isJVBUserType() ? ' id="work"' : '';
   protected function renderBlock(): string
   {
      $ids = (array_key_exists('ids', $this->config) && !empty($this->config['ids'])) ? ' id="'.implode(' ',$this->config['ids']).'"' : '';
      $classes = (array_key_exists('classes', $this->config) && !empty($this->config['classes'])) ? ' class="'.implode(' ',$this->config['classes']).'"' : '';
      $source = (array_key_exists('source', $this->config)) ? ' data-source="'.$this->config['source'].'"' : '';
      $context = (array_key_exists('context', $this->config)) ? ' data-context="'.$this->config['context'].'"' : '';
      $icons = (array_key_exists('icon', $this->config)) ? ' data-icon="'.$this->config['icon'].'"' : ' data-icon="logo-triangle"';
      $gallery = (array_key_exists('is_gallery', $this->config) && $this->config['is_gallery']) ? ' data-gallery' : '';
      $content = (array_key_exists('content', $this->config)) ? ' data-content="'.implode(',',$this->config['content']).'"' : '';
      ob_start();
      ?>
      <section<?= $ids.$classes ?> class="feed-block"<?= $content.$source.$context.$gallery.$icons ?>>
         <?php
         $this->renderFilters();
         $this->renderGrid();
         $this->renderLoader();
         $this->renderTemplates();
         echo TaxonomySelector::outputSelectorModal();
         ?>
      </section>
      <?php
      return ob_get_clean();
   }
        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();
    }
   protected function renderFilters(): void
   {
      if (empty($this->config)) {
         return;
      }
    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' : '';
      $feedContent = $this->getFeedContent();
      $hasMany = count($this->config['content']) > 1;
      ?>
      <form class="feed-filters" data-save="feed-<?=$this->config['context']?>">
         <?php if ($hasMany) {
            //If we have multiple content, only show the content first
            ?>
            <details class="col a-start">
            <summary class="row btw">
               <span class="label">SHOWING: </span>
               <?php
               $labels = [];
               foreach ($this->config['content'] as $i => $type) :
                    $label = $jvb_everything[$type]['plural'];
                    ?>
                    <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>
                <?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>
                  $checked = $i === 0 ? ' checked' : '';
                  $label = $feedContent[$type]['plural'] ?? ucfirst($type);
                  ?>
                  <input type="radio"
                        id="filter-<?= esc_attr($type) ?>"
                        class="btn"
                        name="content"
                        data-filter="content"
                        value="<?= esc_attr($type) ?>"
                     <?= $checked ?>>
                  <label for="filter-<?= esc_attr($type) ?>" title="Show <?= $label ?>" class="row">
                     <?= jvbIcon($feedContent[$type]['icon']) ?>
                     <span class="screen-reader-text"><?= $label ?></span>
                  </label>
                  <?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>
         <?php } ?>
                <?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 (Features::forSite()->has('favourites') && is_user_logged_in()) : ?>
                  <input type="checkbox" id="favourites" class="btn" name="favourites" value="on"
                        data-filter="favourites">
                  <label for="favourites" title="Show Favourites" class="row">
                     <?= jvbIcon('heart').jvbIcon('heart', ['style' => 'fill']) ?>
                     <span class="screen-reader-text">Show Favourites Only</span>
                  </label>
               <?php endif; ?>
            <?php if ($hasMany) { ?>
            </summary>
            <?php } ?>
                <?php if ($many) {
                    echo '</summary>';
                } ?>
               <div class="filters">
                  <div class="filter-group row start">
                     <span class="label">FILTER BY:</span>
                <div class="filters">
                    <div class="filter-group">
                        <span class="label">FILTER BY:</span>
                     <?php
                     $checker = Checker::getInstance();
                     foreach ($this->config['taxonomies'] as $tax) :
                        $taxConfig = JVB_TAXONOMY[$tax] ?? null;
                        if (!$taxConfig) continue;
                        <?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();
                            }
                        $contentForTax = $checker->getContentForTaxonomy($tax);
                        $hidden = empty($contentForTax) ? ' 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>
                        $taxSelector = new TaxonomySelector(
                           'feed-'.$tax,
                           $tax,
                           [
                              'icon'         => $taxConfig['icon']??'logo-triangle',
                              'update'       => '.selected-items-section .selected-items',
                              'types'     => $contentForTax,
                              'autocomplete' => false,
                              'hidden'       => $hidden,
                              'output'    => 'minimal'
                           ]
                        );
                        echo $taxSelector->render();
                     endforeach;
                     ?>
                  </div>
                  <div class="selected-items-section">
                     <div class="selected-items row"></div>
                     <div class="filter-actions row">
                        <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match']) ?>
                        <button type="button" class="clear-filters row">
                           <?= jvbIcon('x') ?>
                           Clear All Filters
                        </button>
                     </div>
                  </div>
               </div>
                <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>
               <div class="row btw nowrap">
                  <div class="order-by filter-group row start w-full">
                     <span class="label">ORDER BY:</span>
                     <?php
                     //TODO: Get content types that can be sorted alphabetically
                     ?>
                     <input type="radio" id="order-title" class="btn" name="orderby" value="title" data-for="artist,shop" data-filter="orderby" hidden>
                     <label for="order-title" title="Order by Name" class="row">
                        <?= jvbIcon('alphabetical') ?>
                        <span class="label">Name</span>
                     </label>
                        <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>
                     <input type="radio" id="order-date" class="btn" name="orderby" value="date" data-filter="orderby" checked>
                     <label for="order-date" title="Order by Date" class="row">
                        <?= jvbIcon('calendar', ['title' => 'Date']) ?>
                        <span class="label">Date</span>
                     </label>
                        <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>
                     <input type="radio" id="order-random" class="btn" name="orderby" value="random" data-filter="orderby">
                     <label for="order-random" title="Random Order" class="row">
                        <?= jvbIcon('shuffle') ?>
                        <span class="label">Random</span>
                     </label>
                  </div>
                    <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>
                  <div class="order-direction filter-group row start w-full" data-for-order="date,title">
                     <span class="label">ORDER:</span>
                     <input type="radio" id="order-desc" class="btn" name="order" value="desc" data-filter="order" checked>
                     <label for="order-desc" title="Sort Descending (A-Z, 1-10)" class="row">
                        <?= jvbIcon('sort-descending') ?>
                        <span class="label" >DESC (A-Z)</span>
                     </label>
                        <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
    }
                     <input type="radio" id="order-asc" class="btn" name="order" value="asc" data-filter="order">
                     <label for="order-asc" title="Sort Ascending (Z-A, 10-1)" class="row">
                        <?= jvbIcon('sort-ascending') ?>
                        <span class="label" >ASC (Z-A)</span>
                     </label>
                  </div>
               </div>
         <?php if ($hasMany) { ?>
            </details>
         <?php } ?>
      </form>
      <?php
   }
    protected function renderGrid():void
    {
        ?>
        <div class="item-grid"></div>
        <?php
    }
   protected function renderGrid(): void
   {
      ?>
      <div class="item-grid"></div>
      <?php
   }
    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>
   protected function renderLoader(): void
   {
      ?>
      <button type="button" class="load-more">
         <?= jvbIcon('arrow-elbow-left-down') ?>
         Show Me More
         <?= jvbIcon('arrow-elbow-right-down') ?>
      </button>
        <?= jvbLoadingScreen() ?>
        <?php
        if (array_key_exists('is_gallery', $this->config)) {
            jvbRenderGallery();
        }
    }
      <?= jvbLoadingScreen() ?>
      <?php
      if (array_key_exists('is_gallery', $this->config)) {
         jvbRenderGallery();
      }
   }
    protected function renderTemplates():void
    {
        echo '<template class="feed-item">
        <details class="item feed" data-umami-event="view_feed">
   protected function renderTemplates(): void
   {
      echo '<template class="feed-item">'.apply_filters('jvbFeedItem', '<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']).'
            .jvbIcon('heart', ['style'=>'fill']).'
                </button>
                <div class="feed-images">
                    <a>
@@ -327,17 +370,23 @@
                    </div>
                </div>
            </div>
        </details>
    </template>';
        </details>', $this->config).'</template>';
        echo '<template class="emptyState">
            <div class="feed-empty-state">
      echo '<template class="emptyState">'.apply_filters('jvbFeedEmptyState', '<div class="feed-empty-state">
                <h3>NOTHING HERE...</h3>
                <p>Try tweaking those filters a bit.</p>
                <p>Edmonton\'s got talent - let\'s find it.</p>
            </div>
        </template>';
            </div>', $this->config). '</template>';
        echo '<template class="placeholderTemplate"><div class="placeholder"></div></template>';
    }
      echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', '').'</div></template>';
   }
   /**
    * Get feed content using Features instead of get_option
    * Returns array of slug => config for types that show in feed
    */
   public function getFeedContent(): array
   {
      return JVB()->routes('feed')->getFeedTypesConfig();
   }
}