<?php
|
namespace JVBase\blocks;
|
|
use JVBase\managers\Cache;
|
use JVBase\registrar\Registrar;
|
use JVBase\base\Site;
|
use JVBase\forms\TaxonomySelector;
|
use JVBase\ui\CRUDSkeleton;
|
use WP_Block;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class FeedBlock
|
{
|
protected Cache $cache;
|
protected array $config;
|
protected string $path = JVB_DIR.'/build/feed';
|
|
protected array $content = [];
|
protected array $taxonomies = [];
|
protected bool $isGallery = false;
|
|
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']
|
]);
|
}
|
|
public function render(array $attributes): string
|
{
|
if (is_post_type_archive(BASE.'directory')) {
|
return '';
|
}
|
$this->determineContent($attributes);
|
if (empty($this->content)) {
|
return '';
|
}
|
$this->determineTaxonomies();
|
$classes = '';
|
|
return sprintf(
|
'<section class="feed-block%s" data-content="%s"%s>
|
%s%s%s%s%s
|
<footer>%s</footer>
|
</section>',
|
$classes,
|
$this->getContent(),
|
$this->isGallery ? ' data-gallery' : '',
|
$this->renderFiltersAndControls(),
|
$this->renderGrid(),
|
$this->renderTemplates(),
|
$this->renderLoader(),
|
TaxonomySelector::outputSelectorModal(),
|
$this->renderActions()
|
);
|
}
|
|
protected function determineContent(array $attrs):void
|
{
|
if (array_key_exists('inheritQuery', $attrs) && $attrs['inheritQuery'] === true) {
|
if (is_post_type_archive()) {
|
$obj = get_queried_object();
|
$this->content = [jvbNoBase($obj->name)];
|
return;
|
} elseif (!empty(Registrar::getProfileTypes()) && is_singular(Registrar::getProfileTypes())) {
|
global $post;
|
$author = $post->post_author;
|
$role = jvbUserRole($author);
|
$registrar = Registrar::getInstance($role);
|
if (!$registrar) {
|
return;
|
}
|
$this->content = $registrar->getCreatable();
|
return;
|
} elseif (is_tax()) {
|
$obj = get_queried_object();
|
$registrar = Registrar::getInstance($obj->taxonomy);
|
if (!$registrar) {
|
return;
|
}
|
if ($registrar->hasFeature('is_content')) {
|
//example: tattoo shop, etc TODO
|
return;
|
}
|
$this->content = array_map(function ($item) { return jvbNoBase($item); }, $registrar->registrar->for);
|
return;
|
}
|
}
|
// not inheriting, getting from config
|
$this->content = $attrs['contentTypes']??[];
|
}
|
protected function getContent():string
|
{
|
return implode(',', $this->content);
|
}
|
protected function determineTaxonomies():void
|
{
|
$taxonomies = [];
|
$ignore = [];
|
foreach ($this->content as $content) {
|
|
$registrar = Registrar::getInstance($content);
|
if (!$registrar) continue;
|
$theTax = $registrar->registrar->taxonomies;
|
foreach ($theTax as $tax) {
|
if (!in_array($tax, $ignore) && !in_array($tax, $taxonomies)) {
|
$taxReg = Registrar::getInstance($tax);
|
if ($taxReg->hasFeature('show_feed')) {
|
$taxonomies[] = $tax;
|
} else {
|
$ignore[] = $tax;
|
}
|
}
|
}
|
}
|
$this->taxonomies = array_unique($taxonomies);
|
}
|
|
protected function renderFiltersAndControls():string
|
{
|
return sprintf(
|
'<details class="all-filters col top left" data-ignore open>
|
<summary>Filters %s</summary>
|
%s%s%s%s%s
|
</details>
|
<button data-action="clear-filters" data-ignore hidden>%s<span>Clear Filters</span></span></button>',
|
$this->renderContentLabels(),
|
$this->renderSearch(),
|
$this->renderContent(),
|
$this->renderFilters(),
|
$this->renderOrderControls(),
|
$this->renderViewControls(),
|
jvbIcon('x')
|
);
|
}
|
protected function renderSearch():string
|
{
|
return sprintf(
|
'<div class="search row left nowrap">
|
<span class="label">Search:</span>
|
%s
|
</div>',
|
jvbSearch()
|
);
|
}
|
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
|
);
|
}
|
protected function renderContent():string
|
{
|
$favourites = '';
|
if (Site::has('favourites')) {
|
$favourites = sprintf(
|
'<input type="checkbox" id="favourites" class="btn" name="favourites" value="on" data-filter="favourites">
|
<label for="favourites" title="Show Favourites">%s%s<span class="screen-reader-text">Show Favourites Only</span></label>',
|
jvbIcon('heart'),
|
jvbIcon('heart', ['style' => 'fill'])
|
);
|
}
|
if (count($this->content) === 1) {
|
return empty($favourites)
|
? sprintf(
|
'<input type="hidden" name="content" value="%s">',
|
implode(',', $this->content)
|
)
|
: sprintf(
|
'<div class="content row right">
|
<input type="hidden" name="content" value="%s">
|
%s
|
</div>',
|
implode(',', $this->content),
|
$favourites
|
);
|
}
|
$i = 0;
|
$content = implode('', array_map(function($type) use (&$i) {
|
$i++;
|
$registrar = Registrar::getInstance($type);
|
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>',
|
$type,
|
$type,
|
$i === 0 ? ' checked' : '',
|
$type,
|
$registrar->getPlural(),
|
jvbIcon($registrar->getIcon()),
|
$registrar->getPlural()
|
);
|
}, $this->content));
|
|
|
|
return sprintf(
|
'<div class="content row left nowrap"><span class="label">Showing:</span>
|
%s%s
|
</div>',
|
$content,
|
$favourites
|
);
|
}
|
|
protected function renderFilters():string
|
{
|
if (empty ($this->taxonomies)) {
|
return '';
|
}
|
$inside = implode('', array_filter(array_map(function($tax) {
|
$registrar = Registrar::getInstance($tax);
|
if (!$registrar) return '';
|
|
$current = BASE.$this->content[0];
|
$contentFor = $registrar->registrar->for;
|
$hidden = in_array($current, $contentFor) ? '' : ' hidden';
|
|
$selector = new TaxonomySelector(
|
'feed-'.$tax,
|
$tax,
|
[
|
'icon' => $registrar->getIcon(),
|
'update'=> '.selected-items-section .selected-items',
|
'types' => $contentFor,
|
'autocomplete' => false,
|
'hidden' => $hidden,
|
'output' => 'minimal',
|
'search' => true
|
]
|
);
|
return $selector->render();
|
|
}, $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>
|
<div class="selected-items-section">
|
<div class="selected-items row left"></div>
|
<div class="filter-actions row">
|
%s
|
<button type="button" class="clear-filters" hidden>
|
%s
|
<span>Clear All Filters</span>
|
</button>
|
</div>
|
</div>
|
</div>',
|
$inside,
|
str_replace('class="toggle-text"', 'class="toggle-text" hidden', jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match'])),
|
jvbIcon('x')
|
);
|
}
|
|
protected function renderOrderControls():string
|
{
|
$orderby = [
|
[
|
'slug' => 'title',
|
'icon' => 'alphabetical',
|
'label' => 'Name'
|
],
|
[
|
'slug' => 'date',
|
'icon' => 'calendar',
|
'label' => 'Date Created',
|
],
|
[
|
'slug' => 'date_modified',
|
'icon' => 'clock-clockwise',
|
'label' => 'Date Modified'
|
]
|
];
|
$custom = $this->getCustomOrdering();
|
$orderby = $orderby + $custom;
|
$orderby[] = [
|
'slug' => 'random',
|
'icon' => 'shuffle',
|
'label' => 'Randomly'
|
];
|
$custom = implode(',', array_map(function($ord) {
|
return $ord['slug'];
|
}, $custom));
|
|
$i = 0;
|
$orderby = sprintf(
|
'<div class="orderby row left">
|
<span class="label">Order by:</span>%s
|
</div>',
|
implode('', array_map(function ($by) use (&$i){
|
$checked = $i === 0 ? ' checked' : '';
|
$i++;
|
return sprintf(
|
'<input type="radio" id="order-%s" class="btn" name="orderby" value="%s" data-filter="orderby"%s%s>
|
<label for="order-%s" title="Order %s">%s<span class="label">%s</span></label>',
|
$by['slug'],
|
$by['slug'],
|
$checked,
|
empty($by['for']??[]) ? '' : ' data-for="'.implode($by['for']).'"',
|
$by['slug'],
|
$by['slug'] === 'random' ? $by['label'] : 'by '.$by['label'],
|
jvbIcon($by['icon']),
|
$by['label']
|
);
|
}, $orderby))
|
);
|
|
$order = [
|
[
|
'slug' => 'desc',
|
'icon' => 'sort-descending',
|
'label' => 'Descending (A-Z, 1-10)'
|
],
|
[
|
'slug' => 'asc',
|
'icon' => 'sort-ascending',
|
'label' => 'Ascending (Z-A, 10-1)'
|
]
|
];
|
|
$i = 0;
|
$order = sprintf(
|
'<div class="order-direction row left" data-for-order="date,date_modified,title%s">
|
<span class="label">Order:</span>
|
%s
|
</div>',
|
$custom === '' ? '' : ','.$custom,
|
implode('', array_map(function ($ord) use (&$i) {
|
$checked = $i=== 0 ? ' checked' : '';
|
$i++;
|
return sprintf(
|
'<input type="radio" id="order-%s" class="btn" name="order" value="%s" data-filter="order"%s>
|
<label for="order-%s" title="Sort %s">
|
%s
|
<span class="label">%s</span>
|
</label>',
|
$ord['slug'],
|
$ord['slug'],
|
$checked,
|
$ord['slug'],
|
$ord['label'],
|
jvbIcon($ord['icon']),
|
$ord['label']
|
);
|
}, $order))
|
);
|
|
return sprintf(
|
'<div class="ordering row left nowrap">%s%s</div>',
|
$orderby,
|
$order
|
);
|
}
|
protected function getCustomOrdering():array
|
{
|
$custom = [];
|
foreach ($this->content as $content) {
|
$registrar = Registrar::getInstance($content);
|
if (!$registrar || empty($registrar->config('feed')->getCustomOrder())) {
|
continue;
|
}
|
$custom = array_merge_recursive($custom, $registrar->config('feed')->getCustomOrder());
|
}
|
return $custom;
|
}
|
protected function renderViewControls():string
|
{
|
$views = [
|
'grid' => ['slug' => 'grid', 'icon' => 'squares-four', 'label' => 'Grid View'],
|
'list' => ['slug' => 'list', 'icon' => 'rows', 'label' => 'List View']
|
];
|
|
$i = 0;
|
return sprintf(
|
'<div class="view row left nowrap"><span class="label">Switch View:</span>%s</div>',
|
implode('', array_map(function ($view) use (&$i) {
|
$checked = $i === 0 ? ' checked' : '';
|
$i++;
|
return sprintf(
|
'<input type="radio"
|
data-view="%s" value="%s" class="btn" name="view" id="view-%s"%s>
|
<label for="view-%s" title="%s">
|
%s<span class="label">%s</span>
|
</label>',
|
$view['slug'],
|
$view['slug'],
|
$view['slug'],
|
$checked,
|
$view['slug'],
|
$view['label'],
|
jvbIcon($view['icon']),
|
$view['label'],
|
);
|
}, $views))
|
);
|
}
|
|
protected function renderGrid():string
|
{
|
$placeholders = '';
|
$total = count($this->content) - 1;
|
$icons = [];
|
$icon = apply_filters('jvbFeedPlaceholder', '');
|
|
for ($i=1; $i<=36; $i++) {
|
if (empty($icon)) {
|
$rand = $total === 0 ? $total : rand(0, $total);
|
$content = $this->content[$rand];
|
if (!in_array($content, $icons)) {
|
$icons[$content] = Registrar::getInstance($content)->getIcon();
|
}
|
$icon = jvbIcon($icons[$content]);
|
}
|
|
$placeholders .= sprintf(
|
'<div class="placeholder">%s</div>',
|
$icon
|
);
|
}
|
|
return sprintf(
|
'<div class="item-grid">%s</div>',
|
$placeholders
|
);
|
}
|
|
protected function renderLoader():string
|
{
|
return sprintf(
|
'<button type="button" class="load-more">%s<span>Show Me More</span>%s</button>
|
%s%s',
|
jvbIcon('arrow-elbow-left-down'),
|
jvbIcon('arrow-elbow-right-down'),
|
jvbLoadingScreen(),
|
$this->isGallery ? jvbRenderGallery(false) : '',
|
);
|
}
|
|
protected function renderTemplates():string
|
{
|
|
$templates = [];
|
foreach ($this->content as $content) {
|
$templates[] = $this->getDefaultTemplate($content);
|
}
|
|
$templates[] = sprintf(
|
'<template class="feedTerm"><button class="remove-term">%s<span></span>%s</button></template>',
|
jvbIcon(jvbDefaultIcon()),
|
jvbIcon('x')
|
);
|
$defaultEmptyState = sprintf(
|
'<div class="empty-state">
|
<h3>%sNOTHING HERE%s</h3>
|
<p>Try tweaking those filters a bit.</p>
|
</div>',
|
jvbIcon(jvbDefaultIcon()),
|
jvbIcon(jvbDefaultIcon()),
|
);
|
$emptyState = apply_filters('jvbFeedEmptyState', $defaultEmptyState, $this->content);
|
$templates[] = sprintf(
|
'<template class="emptyState">%s</template>',
|
$emptyState
|
);
|
|
$placeholder = apply_filters('jvbFeedPlaceholder', jvbIcon(jvbLogoIcon()));
|
$templates[] = sprintf(
|
'<template class="placeholderTemplate"><div class="placeholder">%s</div></template>',
|
$placeholder
|
);
|
|
return implode('', $templates);
|
}
|
|
protected function renderActions():string
|
{
|
return sprintf(
|
'<button data-action="refresh" data-ignore>%s<span>Hard Refresh</span></span></button>',
|
jvbIcon('arrows-clockwise')
|
);
|
}
|
|
public static 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>';
|
}
|
public static 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
|
{
|
$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
|
);
|
}
|
|
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) {
|
'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>',
|
};
|
}
|
|
}
|