directories = $this->getDirectories();
if (empty($this->directories)) {
return;
}
$this->perPage = $perPage;
$this->cache = CacheManager::for('directory', WEEK_IN_SECONDS);
if (JVB_TESTING) {
$this->cache->clear();
}
foreach(['content','taxonomy','user'] as $key) {
if (array_key_exists($key, $this->directories)) {
$this->cache->connectTo($key);
}
}
add_action('init', [$this, 'registerDirectories']);
jvb_register_do_once('directories_registered', [$this, 'activate']);
add_action('render_block', [$this, 'renderBlock'], 99999, 3);
}
public function registerDirectories():void
{
$singular = (array_key_exists('directory_label', JVB_SITE)) ? JVB_SITE['directory_label'][0] : 'Directory';
$plural = (array_key_exists('directory_label', JVB_SITE)) ? JVB_SITE['directory_label'][1] : 'Directories';
$config = [
'labels' => [
'name' => $plural,
'singular_name' => $singular,
'menu_name' => $plural,
'name_admin_bar' => $singular,
'add_new' => "Add New",
'add_new_item' => "Add New {$singular}",
'new_item' => "New {$singular}",
'edit_item' => "Edit {$singular}",
'view_item' => "View {$singular}",
'all_items' => "All {$plural}",
'search_items' => "Search {$plural}",
'parent_item_colon' => "Parent {$plural}:",
'not_found' => "No {$plural} found.",
'not_found_in_trash' => "No {$plural} found in Trash.",
],
'public' => true,
'menu_icon' => jvbCSSIcon('list-dashes'),
'publicly_queryable' => true,
'show_in_menu' => false,
'show_in_admin_bar' => false,
'has_archive' => true,
'hierarchical' => true,
'rewrite' => [
'slug' => sanitize_title(strtolower($plural)),
'with_front' => false,
],
'capability_type' => 'post',
'show_in_rest' => true,
'supports'=>['title', 'author', 'thumbnail', 'editor', 'revisions', 'custom-fields', 'excerpt', 'content']
];
register_post_type(BASE.'directory', $config);
}
public function getDirectories():array
{
$directories = get_option(BASE.'directories');
if (!$directories) {
$directories = [];
//content
if(Features::anyContentHas('show_directory')) {
foreach (JVB_CONTENT as $key => $config) {
if (Features::forContent($key)->has('show_directory')) {
$directories[$key] = 'content';
}
}
}
if(Features::anyTaxonomyHas('show_directory')) {
foreach (JVB_TAXONOMY as $key=>$config) {
if (Features::forTaxonomy($key)->has('show_directory')) {
$directories[$key] = 'taxonomy';
}
}
}
if (Features::anyUserHas('show_directory')) {
foreach(JVB_USER as $key=>$config) {
if (Features::forUser($key)->has('show_directory')) {
$directories[$key] = 'user';
}
}
}
update_option(BASE.'directories', $directories);
}
return $directories;
}
protected function getConfigFromType(string $type):array
{
if (!array_key_exists($type, $this->directories)) {
return [];
}
return match ($this->directories[$type]) {
'content' => JVB_CONTENT[$type],
'taxonomy' => JVB_TAXONOMY[$type],
'user' => JVB_USER[$type],
default => [],
};
}
public function activate()
{
$this->registerDirectories();
$created = [];
$directories = [];
foreach($this->directories as $directory => $type) {
$config = $this->getConfigFromType($directory);
$title = $this->directoryTitle($config);
$excerpt = implode(' ', $config['description']??[]);
$ID = wp_insert_post([
'post_type' => BASE.'directory',
'post_title' => $title,
'post_status' => 'publish',
'post_excerpt' => $excerpt,
'slug' => sanitize_title($title)
]);
if (!is_wp_error($ID)) {
add_post_meta($ID, self::$type, $type);
add_post_meta($ID, self::$slug, $directory);
$created[$directory] = (int)$ID;
$slug = sanitize_title($title);
$directories[$directory] = [
'slug' => $slug,
'title' => $title,
'ID' => $ID,
'url' => get_the_permalink($ID),
'page' => $title,
'description' =>$config[$directory]['description']??[],
'type' => $type,
'extra' => $config[$directory]['directory_extra'] ??[],
];
}
$isGrouped = match ($type) {
'content' => Features::forContent($directory)->has('isGrouped'),
'taxonomy' => Features::forTaxonomy($directory)->has('isGrouped'),
'user' => Features::forUser($directory)->has('isGrouped'),
default => false,
};
if ($isGrouped) {
$title = $title.', but Grouped';
$slug = sanitize_title($title).'-grouped';
$excerpt = $config['groupedDescription']??'Too many options? This is grouped by type.';
$ID = wp_insert_post([
'post_type' => BASE.'directory',
'post_title' => $title,
'post_status' => 'publish',
'post_excerpt' => $excerpt,
'slug' => $slug,
]);
if (!is_wp_error($ID)) {
add_post_meta($ID, self::$type, $type);
add_post_meta($ID, self::$slug, $directory.'-grouped');
add_post_meta($ID, BASE.'grouped_directory', 'yup');
$created[$directory.'-grouped'] = (int)$ID;
$directories[$directory.'-grouped'] = [
'slug' => $slug,
'title' => $title,
'ID' => $ID,
'url' => get_the_permalink($ID),
'page' => $title,
'description' =>$config[$directory]['description']??[],
'type' => $type,
'extra' => $config[$directory]['directory_extra'] ??[],
];
}
}
}
// if (Features::forSite()->has('has_map')) {
// $ID = wp_insert_post([
// 'post_type' => BASE.'directory',
// 'post_title' => 'Map',
// 'post_status'=> 'publish',
// 'slug' => 'map',
// ]);
// if (!is_wp_error($ID)) {
// add_post_meta($ID, self::$type, 'map');
// $created['map'] = (int)$ID;
// $directories['map'] = [
// 'slug' => 'map',
// 'title' => 'Map',
// 'ID' => $ID,
// 'url' => get_the_permalink($ID),
// 'page' => 'Map',
// 'type' => 'term',
// ];
// }
// }
if (!empty($created)) {
update_option(BASE.'directory_ids', $created);
}
if (!empty($directories)) {
update_option(BASE.'directory_list', $directories);
}
}
public function getDirectoryPageIDs():array
{
if (empty($this->directoryPageIDs)) {
$this->directoryPageIDs = get_option(BASE.'directory_ids', []);
}
return $this->directoryPageIDs;
}
public function getDirectoryList():array
{
if (empty($this->directoryList)) {
$this->directoryList = get_option(BASE.'directory_list', []);
}
return $this->directoryList;
}
public static function getConfig(int $ID):array
{
$type = get_post_meta($ID, self::$type, true);
$slug = get_post_meta($ID, self::$slug, true);
return match ($type) {
'content' => JVB_CONTENT[$slug],
'taxonomy' => JVB_TAXONOMY[$slug],
'user' => JVB_USER[$slug],
default => [],
};
}
public function letters():array
{
return [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z'
];
}
public function alphabetizeMe(
array $list,
string $name = '',
string $url = '',
string $ID = '',
$extra = false
):array {
if ($name == '') {
$name = get_the_title();
}
if ($url == '') {
$url = get_the_permalink();
}
if ($ID == '') {
$ID = get_the_ID();
}
$first = strtolower(mb_substr($name, 0, 1));
$list[$first][] = array(
'name' => $name,
'url' => $url,
'id' => $ID,
'extra' => $extra,
);
return $list;
}
public function directories(string $search = 'all'):array
{
$directories = $this->getDirectories();
if ($search === 'all') {
return $directories;
}
return $directories[$search]??[];
}
public function isDirectory():bool
{
return (is_post_type_archive(BASE.'directory') || is_singular(BASE.'directory'));
}
private function renderArchive(): string
{
$this->getDirectoryList();
return $this->cache->remember(
'archive',
function() {
$cache = '
'.$this->referAs().' of '.$this->referAs(true).'
You like lists? We\'ve got \'em!
';
foreach ($this->directoryList as $slug => $directory) {
$config = $this->getConfigFromType($slug);
$aOpen = '';
$aClose = '';
$cache .= '-
'. $aOpen.jvbIcon(array_key_exists('icon', $config) ? $config['icon']:'list-dashes').$directory['title'].$aClose;
if (!empty($directory['description'])) {
$cache .= '
';
foreach ($directory['description'] as $description) {
$cache .= '
'.$description.'
';
}
$cache .= '
';
}
$cache .= ' ';
}
$cache .= '
';
return $cache;
}
);
}
protected function renderIndex(string $current = '', bool $open = false):string
{
$cache = $this->cache->remember(
'index',
function() {
$cache = '';
return $cache;
}
);
if ($current !== '' && array_key_exists($current, $this->directories())) {
$open = ($open) ? ' open' : '';
$cache = 'Other '.$this->referAs(true).':
'.
str_replace('id="'.$current.'"', 'id="'.$current.'" class="current"', $cache)
.' ';
}
return $cache;
}
private function renderDirectory(): string
{
$slug = get_post_meta(get_the_ID(), self::$slug, true);
if ($slug === '') {
return '';
}
$type = $this->directories[$slug];
$config = $this->getConfigFromType($slug);
$paged = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$cacheKey = $slug . '_page_' . $paged;
return $this->cache->remember(
$cacheKey,
function() use ($slug, $type, $config, $paged) {
$out = '' . $this->directoryTitle($config) . '
';
$out .= '';
foreach ($config[$slug]['description'] ?? [] as $p) {
$out .= '
' . $p . '
';
}
$out .= '
';
$out .= $this->renderIndex($slug);
$list = [];
$query = null;
switch ($type) {
case 'content':
$args = [
'post_type' => jvbCheckBase($slug),
'posts_per_page' => $this->perPage,
'paged' => $paged,
'orderby' => 'title',
'order' => 'ASC'
];
if (Features::forContent($slug)->has('is_timeline')) {
$args['post_parent'] = 0;
}
$get = new WP_Query($args);
$hasExtra = Features::forContent($slug)->has('directory_extra');
if ($get->have_posts()) {
while ( $get->have_posts() ) {
$get->the_post();
$extra = [];
if ($hasExtra) {
foreach ($config['directory_extra'] as $item ) {
$item = jvbCheckBase( $item );
$terms = get_the_terms( get_the_ID(), jvbCheckBase( $item ) );
if ( $terms && ! is_wp_error( $terms ) ) {
$term = $terms[0];
$extra[] = [
'name' => (get_term_meta( $term->term_id, BASE . 'singular', true ) !== '') ? get_term_meta( $term->term_id, BASE . 'singular', true ) : $term->name,
'url' => get_term_link( $term->term_id, $item ),
'id' => $term->term_id,
'type' => $item,
];
}
}
}
$list = $this->alphabetizeMe(
$list,
get_the_title(),
get_the_permalink(),
get_the_ID(),
$extra
);
}
}
wp_reset_postdata();
break;
case 'taxonomy':
// For taxonomy, we need to manually paginate
$offset = ($paged - 1) * $this->perPage;
$get = get_terms([
'taxonomy' => jvbCheckBase($slug),
'hide_empty' => true,
'orderby' => 'name',
'order' => 'ASC',
'number' => $this->perPage,
'offset' => $offset,
]);
// Get total for pagination
$total_terms = wp_count_terms([
'taxonomy' => jvbCheckBase($slug),
'hide_empty' => true,
]);
if ($get && !is_wp_error($get)) {
$extra = [];
foreach ($get as $term) {
$list = $this->alphabetizeMe(
$list,
$term->name,
get_term_link( $term->term_id, jvbCheckBase( $slug ) ),
$term->term_id,
$extra
);
}
}
break;
case 'user':
$get = get_users([
'role' => jvbCheckBase($slug),
'orderby' => 'display_name',
'order' => 'ASC',
]);
break;
}
$out .= '';
if (empty($list)) {
$out .= 'Nothing here.
We don\'t have anything here yet.
';
} else {
$out .= $this->renderLettersIndex($list, $type, $slug);
$out .= $this->renderLettersList($list, $slug);
if ($type === 'content' && $query) {
$out .= $this->renderPagination($query);
} elseif ($type === 'taxonomy' && is_numeric($total_terms)) {
$out .= $this->renderTaxonomyPagination($total_terms, $paged);
}
}
$out .= '';
$out .= $this->renderIndex($slug, true);
return $out;
}
);
}
private function renderGroupedDirectory():string
{
$slug = get_post_meta(get_the_ID(), self::$slug, true);
if ($slug === '') {
return '';
}
return $this->cache->remember(
$slug.'_group',
function() use ($slug){
$out = ''.$this->directories[$slug]['title'].'
';
$out .= '';
foreach ($this->directories[$slug]['description']??[] as $p) {
$out .= '
'.$p.'
';
}
$out .= '
';
$out .= $this->renderIndex($slug);
$taxonomy = str_replace('-grouped', '', $slug);
$tax = jvbCheckBase($taxonomy);
$list = $this->renderListChunk($tax, 0);
if ($list !== '') {
$out .= '';
} else {
$out .= 'Nothing here.
We don\'t have anything here yet.
';
}
$out .= $this->renderIndex($slug, true);
return $out;
}
);
}
protected function renderListChunk(string $taxonomy, int $parent):string
{
$get = get_terms([
'taxonomy' => jvbCheckBase(str_replace('-grouped', '', $taxonomy)),
'hide_empty' => true,
'orderby' => 'name',
'order' => 'ASC',
'parent' => $parent
]);
if (!$get || is_wp_error($get)) {
return '';
}
$out = '';
foreach ($get as $term) {
$children =$this->renderListChunk($taxonomy, $term->term_id);
$out .= '- ';
if ($children !== '') {
$out .= '
'.$term->name.'
';
$out .= $children;
$out .= ' ';
} else {
$out .= ''.$term->name.'';
}
$out .= ' ';
}
$out .= '
';
return $out;
}
public function renderLettersIndex(array $list, string $type = '', string $slug = ''): string
{
$letters_on_page = array_keys($list);
$letterPageMap = [];
if ($type !== '' && $slug !== '') {
$letterPageMap = $this->getLetterPageMap($type, $slug);
}
$out = '';
return $out;
}
public function renderLettersList(array $list, string $type):string
{
$umami = JVB()->connect('umami');
$out = '';
foreach ($list as $letter => $items) {
$out .= ''.strtoupper($letter).'
';
}
$out .= '
';
return $out;
}
public function renderBlock(string $content, array $block, WP_Block $instance)
{
if (!is_post_type_archive(BASE.'directory') && !is_singular(BASE.'directory')) {
return $content;
}
if ($block['blockName'] !== 'core/post-content' && ($block['blockName'] !== 'core/group')) {
return $content;
}
// For archive page
if (is_post_type_archive(BASE.'directory') && $block['blockName'] === 'core/group') {
return ($block['attrs']['tagName']??'' === 'main') ? ''.$this->renderArchive().'' : $content;
}
// For single directory posts
if ($block['blockName'] === 'core/group') {
switch (get_post_meta(get_the_ID(), BASE.'grouped_directory', true)) {
case '':
return '' . $this->renderDirectory() . '';
case 'yup':
return '' . $this->renderGroupedDirectory() . '';
}
}
return $content;
}
protected function directoryTitle(array $config):string
{
return array_key_exists('directory', $config) ? $config['directory'] : $config['plural'];
}
public function referAs($plural = false):string
{
if (!empty(JVB_SITE) && array_key_exists('directory_label', JVB_SITE)) {
return ($plural) ? JVB_SITE['directory_label'][1] : JVB_SITE['directory_label'][0];
}
return ($plural) ? 'Directories' : 'Directory';
}
/*****************************************************
* PAGINATION HELPERS
****************************************************/
protected function renderPagination(WP_Query $query): string
{
if ($query->max_num_pages <= 1) {
return '';
}
$current = max(1, isset($_GET['page']) ? (int)$_GET['page'] : 1);
$pagination = paginate_links([
'base' => add_query_arg('page', '%#%'),
'format' => '',
'current' => $current,
'total' => $query->max_num_pages,
'type' => 'array',
'prev_text' => jvbIcon('arrow-square-left'),
'next_text' => jvbIcon('arrow-square-right'),
]);
if (!$pagination) {
return '';
}
$out = '';
return $out;
}
protected function renderTaxonomyPagination(int $total, int $paged): string
{
$max_pages = ceil($total / $this->perPage);
if ($max_pages <= 1) {
return '';
}
$current = max(1, isset($_GET['page']) ? (int)$_GET['page'] : 1);
$pagination = paginate_links([
'base' => add_query_arg('page', '%#%'),
'format' => '',
'current' => $current,
'total' => $max_pages,
'type' => 'array',
'prev_text' => jvbIcon('arrow-square-left'),
'next_text' => jvbIcon('arrow-square-right'),
]);
if (!$pagination) {
return '';
}
$out = '';
return $out;
}
protected function getLetterRanges(array $letters): array
{
if (empty($letters)) {
return [];
}
sort($letters);
$ranges = [];
$start = $letters[0];
$prev = $letters[0];
foreach (array_slice($letters, 1) as $letter) {
// Check if letters are consecutive
if (ord($letter) !== ord($prev) + 1) {
$ranges[] = ['start' => $start, 'end' => $prev];
$start = $letter;
}
$prev = $letter;
}
// Add final range
$ranges[] = ['start' => $start, 'end' => $prev];
return $ranges;
}
protected function getLetterPageMap(string $type, string $slug): array
{
return $this->cache->remember(
$slug . '_letter_page_map',
function() use ($type, $slug) {
$titles = [];
switch ($type) {
case 'content':
global $wpdb;
$post_type = jvbCheckBase($slug);
$where = $wpdb->prepare("post_type = %s AND post_status = 'publish'", $post_type);
if (Features::forContent($slug)->has('is_timeline')) {
$where .= " AND post_parent = 0";
}
$titles = $wpdb->get_col(
"SELECT post_title
FROM {$wpdb->posts}
WHERE {$where}
ORDER BY post_title"
);
break;
case 'taxonomy':
global $wpdb;
$taxonomy = jvbCheckBase($slug);
$titles = $wpdb->get_col($wpdb->prepare(
"SELECT t.name
FROM {$wpdb->terms} t
INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
WHERE tt.taxonomy = %s AND tt.count > 0
ORDER BY t.name ASC",
$taxonomy
));
break;
case 'user':
$users = get_users([
'role' => jvbCheckBase($slug),
'orderby' => 'display_name',
'order' => 'ASC',
'fields' => 'display_name',
]);
$titles = array_column($users, 'display_name');
break;
}
return $this->calculateLetterPages($titles);
}
);
}
protected function calculateLetterPages(array $titles): array
{
$letterCounts = [];
// Count items per letter
foreach ($titles as $title) {
$letter = strtolower(mb_substr($title, 0, 1));
if (!isset($letterCounts[$letter])) {
$letterCounts[$letter] = 0;
}
$letterCounts[$letter]++;
}
// Calculate which page each letter starts on
$letterPages = [];
$runningTotal = 0;
foreach ($this->letters() as $letter) {
if (!isset($letterCounts[$letter])) {
continue;
}
$count = $letterCounts[$letter];
$startPosition = $runningTotal + 1;
$startPage = (int)ceil($startPosition / $this->perPage);
$letterPages[$letter] = [
'page' => $startPage,
'count' => $count,
'start_position' => $startPosition,
];
$runningTotal += $count;
}
return $letterPages;
}
}