Jake Vanderwerf
7 days ago 46d681c6b825d21b3f698d793c4e630c687d90ad
inc/blocks/FeedBlock.php
@@ -1,9 +1,9 @@
<?php
namespace JVBase\blocks;
use JVBase\managers\CacheManager;
use JVBase\utility\Features;
use JVBase\utility\Checker;
use JVBase\managers\Cache;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use JVBase\forms\TaxonomySelector;
use WP_Block;
@@ -13,39 +13,21 @@
class FeedBlock
{
   protected CacheManager $cache;
   protected Cache $cache;
   protected array $config;
   protected string $path = JVB_DIR.'/build/feed';
   public function __construct()
   {
      // Initialize cache with connections
      $this->cache = CacheManager::for('feed_block', WEEK_IN_SECONDS);
      // Set up cache connections for all feed content types
      $this->setupCacheConnections();
      $this->cache = Cache::for('feed_block', WEEK_IN_SECONDS);
//    if (JVB_TESTING) {
//       $this->cache->flush();
//    }
      add_action('init', [$this, 'registerBlock']);
   }
   /**
    * 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);
      }
      // 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);
      }
   }
   public function registerBlock()
   {
      register_block_type($this->path, [
@@ -71,12 +53,13 @@
      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']);
         $registrar = Registrar::getInstance($content)??false;
         if ($registrar) {
            $config = array_merge($config, $registrar->getConfig('feed'));
         } else {
            $config['content'] = $content;
            $config['icon'] = JVB_CONTENT[$content]['icon']??['logo-triangle'];
            $config['icon'] = jvbDefaultIcon();
         }
         if (is_singular()) {
            $config['source'] = $type->ID;
@@ -85,13 +68,13 @@
         $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']);
         $registrar = Registrar::getInstance($content)??false;
         if ($registrar) {
            $config['content'] = $registrar->registrar->for;
            $config['context'] = $content;
            $config['taxonomies'] = $this->getTaxonomies($registrar->registrar->for);
            if (!empty($registrar->getConfig('feed'))){
               $config = array_merge($config, $registrar->getConfig('feed'));
            }
         }
         $config['source'] = $type->term_id;
@@ -106,17 +89,21 @@
   /**
    * 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);
         $registrar = Registrar::getInstance($contentType);
         if (!$registrar) {
            continue;
         }
         $contentTaxonomies = $registrar->registrar->taxonomies;
         $contentTaxonomies = array_filter($contentTaxonomies, function($taxonomy) {
            return array_key_exists('show_feed', JVB_TAXONOMY[$taxonomy]) && JVB_TAXONOMY[$taxonomy]['show_feed'];
            return Registrar::getInstance($taxonomy)?->hasFeature('show_feed');
         });
         $taxonomies = array_merge($taxonomies, $contentTaxonomies);
      }
@@ -126,24 +113,57 @@
   public function render(array $attributes, string $content, WP_Block $block)
   {
      $this->config = $this->buildParams($attributes);
      return $this->cache->remember(
         $this->config,
         $this->cache->generateKey($this->config),
         function() {
            return $this->renderBlock();
         }
      );
   }
   protected function getContext():string|bool
   {
      return array_key_exists('context', $this->config)?$this->config['context']:false;
   }
   protected function getIDS():array|bool
   {
      return (array_key_exists('ids', $this->config) && !empty($this->config['ids'])) ? $this->config['ids'] : false;
   }
   protected function getClasses():array|bool
   {
      return array_key_exists('classes', $this->config) && !empty($this->config['classes']) ? $this->config['classes'] : false;
   }
   protected function getSource():string|bool
   {
      return array_key_exists('source', $this->config) ? $this->config['source'] : false;
   }
   protected function getIcon():string|bool
   {
      return array_key_exists('icon', $this->config) ? $this->config['icon'] : false;
   }
   protected function isGallery():bool
   {
      return (array_key_exists('is_gallery', $this->config) && $this->config['is_gallery']);
   }
   protected function getContent():array|bool
   {
      return (array_key_exists('content', $this->config) && !empty ($this->config['content'])) ? $this->config['content'] : false;
   }
   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']).'"' : '';
      if (is_post_type_archive(BASE.'directory')) {
         return '';
      }
      $ids = ($this->getIDS()) ? ' id="'.implode(' ',$this->getIDS()).'"' : '';
      $classes = ($this->getClasses()) ? ' class="'.implode(' ',$this->getClasses()).'"' : '';
      $source = ($this->getSource()) ? ' data-source="'.$this->getSource().'"' : '';
      $context = ($this->getContext()) ? ' data-context="'.$this->getContext().'"' : '';
      $icons = ($this->getIcon()) ? ' data-icon="'.$this->getIcon().'"' : ' data-icon="'.jvbLogoIcon().'"';
      $gallery = $this->isGallery() ? ' data-gallery' : '';
      $content = ($this->getContent()) ? ' data-content="'.implode(',',$this->getContent()).'"' : '';
      ob_start();
      ?>
      <section<?= $ids.$classes ?> class="feed-block"<?= $content.$source.$context.$gallery.$icons ?>>
@@ -155,6 +175,7 @@
         echo TaxonomySelector::outputSelectorModal();
         ?>
      </section>
      <footer><button data-action="refresh" data-ignore><?=jvbIcon('arrows-clockwise')?><span>Hard Refresh</span></span></button></footer>
      <?php
      return ob_get_clean();
   }
@@ -166,18 +187,18 @@
      }
      $feedContent = $this->getFeedContent();
      $hasMany = count($this->config['content']) > 1;
      $hasMany = count($this->getContent()) > 1;
      ?>
      <form class="feed-filters" data-save="feed-<?=$this->config['context']?>">
      <form class="filters" data-save="feed-<?=$this->getContext()?>">
         <?php if ($hasMany) {
            //If we have multiple content, only show the content first
            ?>
            <details class="col a-start">
            <summary class="row btw">
            <details class="col left">
            <summary class="row x-btw">
               <span class="label">SHOWING: </span>
               <?php
               $labels = [];
               foreach ($this->config['content'] as $i => $type) :
               foreach ($this->getContent() as $i => $type) :
                  $checked = $i === 0 ? ' checked' : '';
                  $label = $feedContent[$type]['plural'] ?? ucfirst($type);
@@ -214,7 +235,7 @@
         <?php } ?>
               <?php if (Features::forSite()->has('favourites') && is_user_logged_in()) : ?>
               <?php if (Site::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">
@@ -224,26 +245,26 @@
               <?php endif; ?>
            <?php if ($hasMany) { ?>
            </summary>
            <?php } ?>
            <?php }
            if (!empty ($this->config['taxonomies'])) {
            ?>
               <div class="filters">
                  <div class="filter-group row start">
                  <div class="filter-group row left">
                     <span class="label">FILTER BY:</span>
                     <?php
                     $checker = Checker::getInstance();
                     foreach ($this->config['taxonomies'] as $tax) :
                        $taxConfig = JVB_TAXONOMY[$tax] ?? null;
                        if (!$taxConfig) continue;
                        $registrar = Registrar::getInstance($tax)??false;
                        if (!$registrar) continue;
                        $contentForTax = $checker->getContentForTaxonomy($tax);
                        $contentForTax = $registrar->registrar->for;
                        $hidden = empty($contentForTax) ? ' hidden' : '';
                        $taxSelector = new TaxonomySelector(
                           'feed-'.$tax,
                           $tax,
                           [
                              'icon'         => $taxConfig['icon']??'logo-triangle',
                              'icon'         => $registrar->getIcon()??jvbLogoIcon(),
                              'update'       => '.selected-items-section .selected-items',
                              'types'     => $contentForTax,
                              'autocomplete' => false,
@@ -258,17 +279,17 @@
                  <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">
                        <?= str_replace('class="toggle-text"', 'class="toggle-text" hidden', jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match'])) ?>
                        <button type="button" class="clear-filters row" hidden>
                           <?= jvbIcon('x') ?>
                           Clear All Filters
                        </button>
                     </div>
                  </div>
               </div>
               <div class="row btw nowrap">
                  <div class="order-by filter-group row start w-full">
            <?php } ?>
               <div class="row x-btw nowrap">
                  <div class="order-by filter-group row left w-full">
                     <span class="label">ORDER BY:</span>
                     <?php
                     //TODO: Get content types that can be sorted alphabetically
@@ -280,19 +301,46 @@
                     </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">
                     <label for="order-date" title="Order by Date Created" class="row">
                        <?= jvbIcon('calendar', ['title' => 'Date']) ?>
                        <span class="label">Date</span>
                        <span class="label">Date Created</span>
                     </label>
                     <input type="radio" id="order-modified" class="btn" name="orderby" value="modified" data-filter="orderby">
                     <label for="order-modified" title="Order by Date Modified" class="row">
                        <?= jvbIcon('clock-clockwise') ?>
                        <span class="label">Date Modified</span>
                     </label>
                     <?php
                        $custom = [];
                        foreach ($this->getContent() as $content) {
                           $registrar = Registrar::getInstance($content)??false;
                           if ($registrar && !empty($registrar->config('feed')->getCustomOrder())) {
                              $custom = array_merge_recursive($custom, $registrar->config('feed')->getCustomOrder());
                           }
                        }
                        foreach ($custom as $slug => $conf) {
                           ?>
                           <input type="radio" id="order-<?=$slug?>" class="btn" name="orderby" value="<?=$slug?>" data-for="<?=$conf['for']?>" data-filter="orderby">
                           <label for="order-<?=$slug?>" title="<?= $conf['label']?>" class="row">
                              <?= jvbIcon($conf['icon']) ?>
                              <span class="label"><?=$conf['label']?></span>
                           </label>
                           <?php
                        }
                        $custom = implode(',', array_keys($custom));
                     ?>
                     <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 filter-group row start w-full" data-for-order="date,title">
                  <div class="order-direction filter-group row left w-full" data-for-order="date,modified,title<?= $custom === '' ? '' : ','.$custom?>">
                     <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">
@@ -319,11 +367,11 @@
      ?>
      <div class="item-grid">
         <?php
         $total = count($this->config['content']) - 1;
         $total = count($this->getContent()) - 1;
         for ($i = 1; $i <= 36; $i++) {
            $rand = rand(0, $total);
            $config = Features::getConfig($this->config['content'][$rand]);
            $icon = jvbIcon($config['icon']??'logo-triangle');
            $config = Registrar::getInstance($this->getContent()[$rand]);
            $icon = jvbIcon($config->getIcon??jvbLogoIcon());
            ?>
            <div class="placeholder"><?=apply_filters('jvbFeedPlaceholder', $icon) ?></div>
            <?php
@@ -351,47 +399,102 @@
   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']).'
                </button>
                <div class="feed-images">
                    <a>
                        <img width="300px" height="300px" loading="lazy" decoding="async">
                    </a>
                </div>
            </summary>
      if ($this->getContent()) {
         foreach ($this->getContent() as $content) {
            echo $this->getDefaultTemplate($content);
         }
      }
            <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>', $this->config).'</template>';
      echo '<template class="emptyState">'.apply_filters('jvbFeedEmptyState', '<div class="feed-empty-state">
                <h3>NOTHING HERE...</h3>
      echo '<template class="feedTerm"><button class="remove-term">'.jvbIcon(jvbDefaultIcon()).'<span></span>'.jvbIcon('x').'</button></template>';
      echo '<template class="emptyState">'.apply_filters('jvbFeedEmptyState', '<div class="empty-state">
                <h3>'.jvbIcon($this->getIcon()).'NOTHING HERE'.jvbIcon($this->getIcon()).'</h3>
                <p>Try tweaking those filters a bit.</p>
                <p>Edmonton\'s got talent - let\'s find it.</p>
            </div>', $this->config). '</template>';
      echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', jvbIcon('logo-triangle')).'</div></template>';
      echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', jvbIcon(jvbLogoIcon())).'</div></template>';
   }
   protected 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>';
   }
   protected 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
   {
      $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 = '<div class="feed item col '.$content.'">'.$this->getFavouritesButton($content).$this->getUpvotesButton($content);
      //Add all defined images, but allow for filtering
      $imageTemplate = '<a>';
      foreach ($images as $image) {
         $imageTemplate .= '<img data-field="'.$image.'" width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">';
      }
      $imageTemplate .= '</a>';
      $template .= '<div class="images">'.apply_filters('jvbFeedImages', $imageTemplate, $content, $images).'</div>';
      //Output default fields, but allow for filtering
      $template .= '<details>
      <summary>'.apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content).'</summary>';
      $fieldsTemplate = '';
      foreach ($fields as $fieldName => $config) {
         $fieldsTemplate .= apply_filters('jvbFeedItemField', $this->defaultFieldTemplate($config['type'], $fieldName), $content, $fieldName, $config['type']);
      }
      $template .= '<div class="item-info">'.apply_filters('jvbFeedItemFields', $fieldsTemplate, $content, $fields).'</div>';
      $template .= '</details></div>';
      return '<template class="feedItem'.ucfirst($content).'">'.apply_filters('jvbFeedItem', $template, $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) {
         '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>',
      };
   }
   /**
    * Get feed content using Features instead of get_option
    * Returns array of slug => config for types that show in feed