Jake Vanderwerf
4 days ago 0afb2c0046b55c123eafb4ab9ee77efa68d12463
inc/blocks/FeedBlock.php
@@ -2,6 +2,7 @@
namespace JVBase\blocks;
use JVBase\managers\Cache;
use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use JVBase\forms\TaxonomySelector;
@@ -25,6 +26,9 @@
   protected ?int $contextID = null;
   protected bool $isGallery = false;
   protected array $args;
   protected bool $isContentTax = false;
   protected bool $hasMore = false;
   public function __construct()
   {
@@ -33,6 +37,7 @@
      if (JVB_TESTING) {
         $this->cache->flush();
      }
      $this->cache->flush();
      add_action('init', [$this, 'registerBlock']);
   }
@@ -78,15 +83,26 @@
   protected function determineContent(array $attrs):void
   {
      if (array_key_exists('inheritQuery', $attrs) && $attrs['inheritQuery'] === true) {
         if (is_post_type_archive()) {
         $args = [
            'posts_per_page'  => 36,
            'fields'       => 'ids',
         ];
         if (is_post_type_archive() &&!is_tax()) {
            $obj = get_queried_object();
            $registrar = Registrar::getInstance($obj->name);
            if ($registrar && $registrar->hasFeature('is_timeline')) {
               $args['post_parent'] = 0;
            }
            $this->content = [jvbNoBase($obj->name)];
            $args['post_type'] = $obj->name;
            $this->args = $args;
            return;
         } elseif (!empty(Registrar::getProfileTypes()) && is_singular(Registrar::getProfileTypes())) {
            global $post;
            $author = $post->post_author;
            $role = jvbUserRole($author);
            $args['post_author'] = $author;
            $this->context = jvbNoBase($role);
            $this->contextID = $author;
            $registrar = Registrar::getInstance($role);
@@ -94,25 +110,50 @@
               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
      {
@@ -212,7 +253,6 @@
               'fields' => 'ids',
            ];
            if (!is_null($this->context)) {
               $context = Registrar::getInstance($this->context);
               switch ($context->getType()) {
                  case 'term':
@@ -229,6 +269,7 @@
            }
            $check = new WP_Query($args);
            $hasPosts = !empty($check->posts);
            wp_reset_postdata();
            $disabled = !$hasPosts;
            $checked = $i === 0 && $hasPosts;
@@ -463,7 +504,7 @@
         );
      }
   protected function renderGrid():string
   protected function renderPlaceholders():string
   {
      $placeholders = '';
      $total = count($this->content) - 1;
@@ -492,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) : '',
      );
@@ -542,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
@@ -554,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
   {
@@ -565,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);
   }
}