From 0afb2c0046b55c123eafb4ab9ee77efa68d12463 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 06 Jun 2026 17:15:31 +0000
Subject: [PATCH] =Starting the Favourites.js setup, converting previous Northeh stuff to new Registrar, fixing up Square.php integration to match
---
inc/blocks/FeedBlock.php | 484 +++++++++++++++++++++++++++++++++++++++++------------
1 files changed, 372 insertions(+), 112 deletions(-)
diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 3ed2f38..abe8395 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -2,11 +2,13 @@
namespace JVBase\blocks;
use JVBase\managers\Cache;
+use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use JVBase\forms\TaxonomySelector;
use JVBase\ui\CRUDSkeleton;
use WP_Block;
+use WP_Query;
if (!defined('ABSPATH')) {
exit;
@@ -20,7 +22,13 @@
protected array $content = [];
protected array $taxonomies = [];
+ protected ?string $context = null;
+ protected ?int $contextID = null;
+
protected bool $isGallery = false;
+ protected array $args;
+ protected bool $isContentTax = false;
+ protected bool $hasMore = false;
public function __construct()
{
@@ -29,6 +37,7 @@
if (JVB_TESTING) {
$this->cache->flush();
}
+ $this->cache->flush();
add_action('init', [$this, 'registerBlock']);
}
@@ -53,13 +62,15 @@
$classes = '';
return sprintf(
- '<section class="feed-block%s" data-content="%s"%s>
+ '<section class="feed-block%s" data-content="%s"%s%s%s>
%s%s%s%s%s
<footer>%s</footer>
</section>',
$classes,
$this->getContent(),
$this->isGallery ? ' data-gallery' : '',
+ is_null($this->context) ? '' : ' data-context="'.$this->context.'"',
+ is_null($this->contextID) ? '' : ' data-context-id="'.$this->contextID.'"',
$this->renderFiltersAndControls(),
$this->renderGrid(),
$this->renderTemplates(),
@@ -72,36 +83,77 @@
protected function determineContent(array $attrs):void
{
if (array_key_exists('inheritQuery', $attrs) && $attrs['inheritQuery'] === true) {
- if (is_post_type_archive()) {
+ $args = [
+ 'posts_per_page' => 36,
+ 'fields' => 'ids',
+ ];
+ if (is_post_type_archive() &&!is_tax()) {
$obj = get_queried_object();
+ $registrar = Registrar::getInstance($obj->name);
+ if ($registrar && $registrar->hasFeature('is_timeline')) {
+ $args['post_parent'] = 0;
+ }
$this->content = [jvbNoBase($obj->name)];
+ $args['post_type'] = $obj->name;
+ $this->args = $args;
return;
} elseif (!empty(Registrar::getProfileTypes()) && is_singular(Registrar::getProfileTypes())) {
global $post;
$author = $post->post_author;
$role = jvbUserRole($author);
+
+ $args['post_author'] = $author;
+ $this->context = jvbNoBase($role);
+ $this->contextID = $author;
$registrar = Registrar::getInstance($role);
if (!$registrar) {
return;
}
$this->content = $registrar->getCreatable();
+ $args['post_type'] = array_map('jvbCheckBase', $this->content);
+ foreach($args['post_type'] as $post_type) {
+ $reg = Registrar::getInstance($post_type);
+ if ($reg && $reg->hasFeature('is_timeline')) {
+ $args['post_parent'] = 0;
+ }
+ }
+ $this->args = $args;
return;
} elseif (is_tax()) {
$obj = get_queried_object();
+ $this->context = jvbNoBase($obj->taxonomy);
+ $this->contextID = $obj->term_id;
+ $args['tax_query'] = [];
+ $args['tax_query'][] = [
+ 'taxonomy' => $obj->taxonomy,
+ 'terms' => $obj->term_id,
+ ];
$registrar = Registrar::getInstance($obj->taxonomy);
if (!$registrar) {
return;
}
if ($registrar->hasFeature('is_content')) {
- //example: tattoo shop, etc TODO
+ $this->isContentTax = true;
+ $this->content = [$registrar->getBased()];
return;
}
$this->content = array_map(function ($item) { return jvbNoBase($item); }, $registrar->registrar->for);
+ $args['post_type'] = array_map('jvbCheckBase', $registrar->registrar->for);
+ foreach($args['post_type'] as $post_type) {
+ $reg = Registrar::getInstance($post_type);
+ if ($reg && $reg->hasFeature('is_timeline')) {
+ $args['post_parent'] = 0;
+ }
+ }
+ $this->args = $args;
return;
}
}
// not inheriting, getting from config
$this->content = $attrs['contentTypes']??[];
+
+ $args['post_type'] = array_map('jvbCheckBase', $attrs['contentTypes']);
+ $this->args = $args;
}
protected function getContent():string
{
@@ -133,7 +185,7 @@
protected function renderFiltersAndControls():string
{
return sprintf(
- '<details class="all-filters col top left" data-ignore open>
+ '<details class="all-filters col top left" data-ignore>
<summary>Filters %s</summary>
%s%s%s%s%s
</details>
@@ -159,23 +211,11 @@
}
protected function renderContentLabels():string
{
- $inside = '';
- if (count($this->content) === 1) {
- return '';
- }
- foreach ($this->content as $i => $type) {
- $active = $i === 0 ? ' class="active"' : '';
- $inside .= sprintf(
- '<li id="filter-%s"%s>%s</li>',
- $type,
- $active,
- Registrar::getInstance($type)->getPlural()??''
- );
- }
return sprintf(
- '<ul class="filter-label">%s</ul>',
- $inside
+ '<span class="label">Showing: <span class="current">%s</span></span>',
+ $this->content[0]??''
);
+
}
protected function renderContent():string
{
@@ -191,7 +231,7 @@
if (count($this->content) === 1) {
return empty($favourites)
? sprintf(
- '<input type="hidden" name="content" value="%s">',
+ '<input type="hidden" data-filter="content" name="content" value="%s">',
implode(',', $this->content)
)
: sprintf(
@@ -205,23 +245,55 @@
}
$i = 0;
$content = implode('', array_map(function($type) use (&$i) {
- $i++;
$registrar = Registrar::getInstance($type);
+
+ $args = [
+ 'post_type' => $registrar->getBased(),
+ 'posts_per_page' => 1,
+ 'fields' => 'ids',
+ ];
+ if (!is_null($this->context)) {
+ $context = Registrar::getInstance($this->context);
+ switch ($context->getType()) {
+ case 'term':
+ $args['tax_query'] = [];
+ $args['tax_query'][] = [
+ 'taxonomy' => $context->getBased(),
+ 'terms' => $this->contextID
+ ];
+ break;
+ case 'user':
+ $args['author'] = $this->contextID;
+ break;
+ }
+ }
+ $check = new WP_Query($args);
+ $hasPosts = !empty($check->posts);
+ wp_reset_postdata();
+
+ $disabled = !$hasPosts;
+ $checked = $i === 0 && $hasPosts;
+ if ($hasPosts) {
+ $i++;
+ }
return sprintf(
'<input type="radio"
id="filter-%s"
class="btn"
name="content"
data-filter="content"
- value="%s"%s>
- <label for="filter-%s" title="Show %s">%s<span class="screen-reader-text">%s</span></label>',
+ data-label="%s"
+ value="%s"%s%s>
+ <label for="filter-%s" title="Show %s">%s<span class="label">%s</span></label>',
$type,
+ $registrar->getSingular(),
$type,
- $i === 0 ? ' checked' : '',
+ $checked ? ' checked' : '',
+ $disabled ? ' disabled' : '',
$type,
- $registrar->getPlural(),
+ $registrar->getSingular(),
jvbIcon($registrar->getIcon()),
- $registrar->getPlural()
+ $registrar->getSingular()
);
}, $this->content));
@@ -267,9 +339,9 @@
}, $this->taxonomies)));
return sprintf(
'<div class="taxonomies row left">
- <div class="row top nowrap">
- <span class="label">Filter By:</span>
- <div class="row left">%s</div>
+ <div class="row top wrap">
+ <span class="label">Filter By:</span>
+ %s
</div>
<div class="selected-items-section">
<div class="selected-items row left"></div>
@@ -314,6 +386,7 @@
'icon' => 'shuffle',
'label' => 'Randomly'
];
+
$custom = implode(',', array_map(function($ord) {
return $ord['slug'];
}, $custom));
@@ -382,7 +455,7 @@
);
return sprintf(
- '<div class="ordering row left nowrap">%s%s</div>',
+ '<div class="ordering row top left nowrap">%s%s</div>',
$orderby,
$order
);
@@ -431,7 +504,7 @@
);
}
- protected function renderGrid():string
+ protected function renderPlaceholders():string
{
$placeholders = '';
$total = count($this->content) - 1;
@@ -460,13 +533,58 @@
);
}
+
+ protected function renderGrid():string
+ {
+ if (empty($this->args)) {
+ return $this->renderPlaceholders();
+ }
+ $items = $this->getItems();
+
+ $out = '<div class="item-grid">';
+ $out .= implode('',array_map(function ($ID) {
+ $content = $this->isContentTax
+ ? jvbNoBase($this->content[0])
+ : jvbNoBase(get_post_type($ID));
+
+ return $this->renderItem($content, $ID);
+ }, $items));
+ $out .= '</div>';
+ return $out;
+ }
+ protected function getItems():array
+ {
+ if ($this->isContentTax) {
+ $items = get_terms([
+ 'taxonomy' => $this->content,
+ 'fields' => 'ids',
+ 'meta_key' => BASE.'date_modified',
+ 'meta_type' => 'DATETIME',
+ 'orderby' => 'meta_value',
+ 'order' => 'desc',
+ 'number' => $this->args['posts_per_page']
+ ]);
+ $items = $items && !is_wp_error($items) ? $items : [];
+ } else {
+ $items = new WP_Query($this->args);
+ $this->hasMore = $items->found_posts > $this->args['posts_per_page'];
+ $items = $items->posts;
+ wp_reset_postdata();
+ }
+ return $items;
+ }
+
protected function renderLoader():string
{
- return sprintf(
- '<button type="button" class="load-more">%s<span>Show Me More</span>%s</button>
- %s%s',
+ $button = sprintf(
+ '<button type="button" class="load-more"%s>%s<span>Show Me More</span>%s</button>',
+ $this->hasMore ? '' : ' hidden',
jvbIcon('arrow-elbow-left-down'),
jvbIcon('arrow-elbow-right-down'),
+ );
+ return sprintf(
+ '%s%s%s',
+ $button,
jvbLoadingScreen(),
$this->isGallery ? jvbRenderGallery(false) : '',
);
@@ -510,10 +628,10 @@
protected function renderActions():string
{
- return sprintf(
+ return is_user_logged_in() ? sprintf(
'<button data-action="refresh" data-ignore>%s<span>Hard Refresh</span></span></button>',
jvbIcon('arrows-clockwise')
- );
+ ) : '';
}
public static function getFavouritesButton(string $content):string
@@ -522,10 +640,11 @@
if (!$registrar || !Site::has('favourites') || !$registrar->hasFeature('favouritable')) {
return '';
}
- return '<button class="favourite" type="button" title="Add to favourites" data-action="favourite">
- '.jvbIcon('heart')
- .jvbIcon('heart', ['style'=>'fill']).'
- </button>';
+ return sprintf(
+ '<button class="favourite" type="button" title="Add to favourites" data-action="favourite">%s%s</button>',
+ jvbIcon('heart'),
+ jvbIcon('heart', ['style'=>'fill'])
+ );
}
public static function getUpvotesButton(string $content):string
{
@@ -533,98 +652,239 @@
if (!Site::has('karma') || !$registrar || !$registrar->hasFeature('karma')){
return '';
}
- return '<div class="karma row">
+ return sprintf(
+ '<div class="karma row">
<button type="button" class="vote" data-action="upvote">
- '.jvbIcon('arrow-fat-up')
- .jvbIcon('arrow-fat-up', ['style'=>'fill']).
- '</button>
+ %s%s
+ </button>
<button type="button" class="vote" data-action="downvote">
- '.jvbIcon('arrow-fat-down')
- .jvbIcon('arrow-fat-down', ['style'=>'fill']).
- '</button>
+ %s%s
+ </button>
<span class="score"></span>
- </div>';
+ </div>',
+ jvbIcon('arrow-fat-up'),
+ jvbIcon('arrow-fat-up', ['style'=>'fill']),
+ jvbIcon('arrow-fat-down'),
+ jvbIcon('arrow-fat-down', ['style'=>'fill'])
+ );
}
protected function getDefaultTemplate(string $content): string
{
- $template = apply_filters('jvbFeedItem', '', $content);
- if (empty($template)) {
- $config = Registrar::getInstance($content)->getConfig('feed');
- $allFields = Registrar::getFieldsFor($content);
- $images = $config['images']??['post_thumbnail'];
- $fields = $config['fields']??['post_title','post_date','post_excerpt'];
- $fields = array_filter($fields, function($field) use($images) {
- return !in_array($field, $images);
- });
- $fields = array_filter($allFields, function($field) use($fields) {
- return in_array($field, $fields);
- }, ARRAY_FILTER_USE_KEY);
-
-
- $template = sprintf(
- '<div class="feed item col %s">%s%s',
- $content,
- self::getFavouritesButton($content),
- self::getUpvotesButton($content)
- );
-
- //Add all defined images, but allow for filtering
- $imageTemplate = '<a>';
- foreach ($images as $image) {
- $imageTemplate .= sprintf(
- '<img data-field="%s" width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
- $image
- );
- }
- $imageTemplate .= '</a>';
-
- $template .= sprintf(
- '<div class="images">%s</div>',
- apply_filters('jvbFeedImages', $imageTemplate, $content, $images)
- );
-
- //Output default fields, but allow for filtering
- $template .= sprintf(
- '<details>
- <summary>%s</summary>',
- apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content)
- );
-
- $fieldsTemplate = '';
- foreach ($fields as $fieldName => $config) {
- $fieldsTemplate .= apply_filters('jvbFeedItemField', $this->defaultFieldTemplate($config['type'], $fieldName), $content, $fieldName, $config['type']);
- }
- $template .= sprintf(
- '<div class="item-info">%s</div>',
- apply_filters('jvbFeedItemFields', $fieldsTemplate, $content, $fields)
- );
- $template .= '</details></div>';
- }
-
-
return sprintf(
'<template class="feedItem%s">%s</template>',
ucfirst($content),
- $template
+ $this->renderItem($content)
);
}
- protected function defaultFieldTemplate(string $fieldType, string $fieldName):string
+ protected function renderItem(string $content, ?int $ID = null):string
+ {
+ /**
+ * Allow plugins to replace the content within the feed item
+ */
+ $function = BASE.'render_'.$content.'_feed_item';
+ if (function_exists($function)) {
+ $out = $function($ID);
+ } else {
+ $out = $this->buildFeedItem($content, $ID);
+ }
+ $registrar = Registrar::getInstance($content);
+ $data = [];
+ if ($registrar && $registrar->hasFeature('is_timeline')) {
+ $data[] = 'data-timeline';
+ }
+
+ $data = !empty($data) ? ' '.implode(' ', $data) : '';
+
+ return sprintf(
+ '<div class="feed item col %s"%s>%s%s%s</div>',
+ $content,
+ $data,
+ self::getFavouritesButton($content),
+ self::getUpvotesButton($content),
+ $out
+ );
+ }
+ protected function buildFeedItem(string $content, ?int $ID = null):string
+ {
+ $registrar = Registrar::getInstance($content);
+ $meta = is_null($ID) ? false :
+ match ($registrar->getType()) {
+ 'post' => Meta::forPost($ID),
+ 'term' => Meta::forTerm($ID),
+ 'user' => Meta::forUser($ID),
+ default => false
+ };
+
+ [$images, $fields] = $registrar->getFeedFields();
+
+ /**
+ * Get the main image for the feed item.
+ * Output can be overridden with the $imagesFn
+ */
+ $imagesFn = BASE.'render_'.$content.'_feed_item_images';
+ if (function_exists($imagesFn)) {
+ $img = $imagesFn($ID, $images);
+ } else {
+ $img = '';
+ foreach ($images as $config) {
+ $field = $config['name'];
+ $img .= $meta ? jvbFormatImage($meta->get($field), 'tiny', 'medium')
+ : $this->defaultFieldTemplate($config['type'], $field);
+ }
+ }
+ $img = sprintf(
+ '<div class="images"><a href="%s">%s</a></div>',
+ $ID ? get_the_permalink($ID) : '',
+ $img
+ );
+
+ /**
+ * Start the Details with the fields
+ * Plugins can modify the summary title with the 'jvbFeedItemSummary' filter
+ */
+ $summary = sprintf(
+ '<details><summary>%s</summary>',
+ apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content)
+ );
+ /**
+ * Work through the fields
+ * Each field output can be overridden with $field or $fieldType
+ */
+ foreach ($fields as $config) {
+ $f = $config['name'];
+ $functions = [
+ BASE.'render_'.$content.'_field_'.$f, //Overrides field for this content
+ BASE.'render_field_'.$f, //Overrides field with this name
+ BASE.'render_field_type_'.$config['type'] //Overrides field of this type
+ ];
+
+ $didIt = false;
+ foreach ($functions as $func) {
+ if (!$didIt && function_exists($func)) {
+ $didIt = true;
+ $summary .= $func($ID, is_null($ID));
+ }
+ }
+ if (!$didIt) {
+ $summary .= $this->defaultFieldTemplate($config['type'], $f, is_null($ID), $meta, $config);
+ }
+
+ }
+ $summary .= '</details>';
+
+ return $img.$summary;
+ }
+
+ protected function defaultFieldTemplate(string $fieldType, string $fieldName, bool $isTemplate = true, bool|Meta $meta = false, array $config = []):string
{
$data = ' data-field="'.$fieldName.'"';
+ $value = $meta ? $meta->get($fieldName) : '';
+ if (!$isTemplate && empty($value)) {
+ return '';
+ }
switch ($fieldName) {
case 'post_title':
- return '<h3'.$data.'></h3>';
+ return sprintf(
+ '<h3%s>%s</h3>',
+ $data,
+ $meta && !empty($value) ? $value : '',
+ );
case 'post_date':
case 'post_modified':
- return '<time'.$data.'></time>';
+ return sprintf(
+ '<time%s%s>%s</time>',
+ $data,
+ $meta && !empty($value) ? date('c', strtotime($value)) : '',
+ $meta && !empty($value) ? date('F j, Y', strtotime($value)) : '',
+ );
}
- return match($fieldType) {
- 'date','datetime','time' => '<time'.$data.'></time>',
- 'upload' => '<img'.$data.' width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
- 'taxonomy' => '<ul'.$data.'><li><a><i></i></a></li></ul>',
- default => '<p'.$data.'></p>',
+
+ return match ($fieldType) {
+ 'date' => sprintf(
+ '<time%s%s>%s</time>',
+ $data,
+ $meta && !empty($value) ? date('c', strtotime($value)) : '',
+ $meta && !empty($value) ? date('F j, Y', strtotime($value)) : '',
+ ),
+ 'datetime' => sprintf(
+ '<time%s%s>%s</time>',
+ $data,
+ $meta && !empty($value) ? date('c', strtotime($value)) : '',
+ $meta && !empty($value) ? date('F j, Y at h:iA', strtotime($value)) : '',
+ ),
+ 'time' => sprintf(
+ '<time%s%s>%s</time>',
+ $data,
+ $value,
+ $value
+ ),
+ 'upload' => empty($value) ? sprintf(
+ '<img%s width="300px" height="300px" loading="lazy" decoding="async">',
+ $data
+ ) :
+ str_replace('<img', '<img' . $data, jvbFormatImage($value)),
+ 'selector' => sprintf(
+ '<ul%s class="terms %s">%s</ul>',
+ $data,
+ $config['taxonomy']??$config['post_type']??$config['role']??'',
+ empty($value) ? sprintf('<li><a>%s</a></li>',
+ $this->iconFor($config)
+ ) :
+ implode('', array_filter(array_map(function ($ID) use ($config) {
+ $type = $config['subtype'];
+ $taxonomy = isset($config['taxonomy']) ? jvbCheckBase($config['taxonomy']) : '';
+
+ $item = match ($type) {
+ 'taxonomy' => get_term($ID, $taxonomy),
+ 'user' => get_userdata($ID),
+ 'post' => get_post($ID),
+ default => false,
+ };
+
+ if (!$item || is_wp_error($item)) {
+ return '';
+ }
+
+ $icon = $this->iconFor($config);
+
+ $name = match ($type) {
+ 'taxonomy' => $item->name,
+ 'user' => $item->display_name,
+ 'post' => $item->post_title,
+ default => '',
+ };
+ $url = match ($type) {
+ 'taxonomy' => get_term_link((int)$ID, $taxonomy),
+ 'user' => get_the_permalink(get_user_meta($ID, BASE . 'userLink', true)),
+ 'post' => get_the_permalink($ID),
+ default => ''
+ };
+ return sprintf(
+ '<li><a href="%s">%s%s</a></li>',
+ $url,
+ !empty($icon) ? jvbIcon($icon) : '',
+ $name
+ );
+ }, explode(',', $value))))
+ ),
+ default => sprintf(
+ '<p%s>%s</p>',
+ $data,
+ $value
+ ),
};
}
+ protected function iconFor(array $fieldConfig):string
+ {
+ $icon = match ($fieldConfig['type']??'') {
+ 'taxonomy' => Registrar::getInstance($fieldConfig['taxonomy'])->getIcon(),
+ 'user' => isset($fieldConfig['role']) ? Registrar::getInstance($fieldConfig['role'])->getIcon() : 'user',
+ 'post' => Registrar::getInstance($fieldConfig['post_type'])->getIcon(),
+ default => ''
+ };
+
+ return empty($icon) ? $icon : jvbIcon($icon);
+ }
}
--
Gitblit v1.10.0