<?php
|
namespace JVBase\blocks;
|
|
use JVBase\managers\Cache;
|
use JVBase\utility\Features;
|
use JVBase\utility\Checker;
|
use JVBase\forms\TaxonomySelector;
|
use WP_Block;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class FeedBlock
|
{
|
protected Cache $cache;
|
protected array $config;
|
protected string $path = JVB_DIR.'/build/feed';
|
|
public function __construct()
|
{
|
// Initialize cache with connections
|
$this->cache = Cache::for('feed_block', WEEK_IN_SECONDS);
|
// if (JVB_TESTING) {
|
// $this->cache->flush();
|
// }
|
|
add_action('init', [$this, 'registerBlock']);
|
}
|
|
public function registerBlock()
|
{
|
register_block_type($this->path, [
|
'render_callback' => [$this, 'render']
|
]);
|
}
|
|
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();
|
|
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']??[jvbLogoIcon()];
|
}
|
if (is_singular()) {
|
$config['source'] = $type->ID;
|
}
|
|
$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;
|
}
|
|
if (!is_array($config['content'])) {
|
$config['content'] = [$config['content']];
|
}
|
|
return $config;
|
}
|
|
/**
|
* 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->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
|
{
|
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 ?>>
|
<?php
|
$this->renderFilters();
|
$this->renderGrid();
|
$this->renderLoader();
|
$this->renderTemplates();
|
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();
|
}
|
|
protected function renderFilters(): void
|
{
|
if (empty($this->config)) {
|
return;
|
}
|
|
$feedContent = $this->getFeedContent();
|
$hasMany = count($this->getContent()) > 1;
|
?>
|
<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">
|
<span class="label">SHOWING: </span>
|
<?php
|
$labels = [];
|
foreach ($this->getContent() as $i => $type) :
|
|
$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 (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 }
|
if (!empty ($this->config['taxonomies'])) {
|
?>
|
<div class="filters">
|
<div class="filter-group row start">
|
<span class="label">FILTER BY:</span>
|
|
<?php
|
$checker = Checker::getInstance();
|
foreach ($this->config['taxonomies'] as $tax) :
|
$taxConfig = JVB_TAXONOMY[$tax] ?? null;
|
if (!$taxConfig) continue;
|
|
$contentForTax = $checker->getContentForTaxonomy($tax);
|
$hidden = empty($contentForTax) ? ' hidden' : '';
|
|
$taxSelector = new TaxonomySelector(
|
'feed-'.$tax,
|
$tax,
|
[
|
'icon' => $taxConfig['icon']??jvbLogoIcon(),
|
'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">
|
<?= 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>
|
<?php } ?>
|
<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" data-filter="orderby" checked>
|
<label for="order-date" title="Order by Date Created" class="row">
|
<?= jvbIcon('calendar', ['title' => 'Date']) ?>
|
<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) {
|
$config = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??false;
|
if ($config && array_key_exists('custom_order', $config)) {
|
$custom = array_merge_recursive($custom, $config['custom_order']);
|
}
|
}
|
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,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">
|
<?= jvbIcon('sort-descending') ?>
|
<span class="label" >DESC (A-Z)</span>
|
</label>
|
|
<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">
|
<?php
|
$total = count($this->getContent()) - 1;
|
for ($i = 1; $i <= 36; $i++) {
|
$rand = rand(0, $total);
|
$config = Features::getConfig($this->getContent()[$rand]);
|
$icon = jvbIcon($config['icon']??jvbLogoIcon());
|
?>
|
<div class="placeholder"><?=apply_filters('jvbFeedPlaceholder', $icon) ?></div>
|
<?php
|
}
|
?>
|
</div>
|
<?php
|
}
|
|
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();
|
}
|
}
|
|
protected function renderTemplates(): void
|
{
|
if ($this->getContent()) {
|
foreach ($this->getContent() as $content) {
|
echo $this->getDefaultTemplate($content);
|
}
|
}
|
|
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>
|
</div>', $this->config). '</template>';
|
echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', jvbIcon(jvbLogoIcon())).'</div></template>';
|
}
|
|
protected function getFavouritesButton(string $content):string
|
{
|
if (!Features::forSite()->has('favourites') && !Features::forContent($content)->has('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
|
{
|
if (!Features::forSite()->has('karma') && !Features::forContent($content)->has('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 = JVB_CONTENT[$content]??[];
|
$hasConfig = array_key_exists('feed', $config);
|
$images = ($hasConfig && array_key_exists('images', $config['feed']) ? $config['feed']['images']:['post_thumbnail']);
|
$images = (is_array($images)) ? $images : [$images];
|
$fields = ($hasConfig && array_key_exists('fields', $config['feed']) ? $config['feed']['fields']:['post_title', 'post_date']);
|
$fields = array_filter($fields, function($field) use($images) {
|
return !in_array($field, $images);
|
});
|
$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) {
|
$fieldType = JVB_CONTENT[$content][$fieldName]['type']??'text';
|
$fieldsTemplate .= apply_filters('jvbFeedItemField', $this->defaultFieldTemplate($fieldType, $fieldName), $content, $fieldName, $fieldType);
|
}
|
$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
|
*/
|
public function getFeedContent(): array
|
{
|
return JVB()->routes('feed')->getFeedTypesConfig();
|
}
|
}
|