Jake Vanderwerf
2026-04-26 86c6cd3cc099d2480932ede03c12cea01e625c94
inc/managers/DirectoryManager.php
@@ -5,157 +5,208 @@
   exit;
}
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use WP_Block;
use WP_Query;
class DirectoryManager
{
    protected array $directories;
    protected array $directoryPageIDs;
   protected array $directoryList;
   protected int $perPage;
    protected static string $type = BASE.'for_type';
    protected static string $slug = BASE.'for_type_slug';
    protected CacheManager $cache;
    protected Cache $cache;
    public function __construct()
    public function __construct($perPage = 100)
    {
        $this->directories = jvbGlobalDirectoryInfo();
        if (empty(jvbGlobalDirectories())) {
      $this->directories = $this->getDirectories();
        if (empty($this->directories)) {
            return;
        }
        $this->cache = new CacheManager('directory', WEEK_IN_SECONDS);
      $this->perPage = $perPage;
        $this->cache = Cache::for('directory', WEEK_IN_SECONDS);
      $this->cache->connect('post', true)
         ->connect('taxonomy', true)
         ->connect('user', true);
        add_action('init', [$this, 'registerDirectories']);
        jvb_register_do_once('directories_registered', [$this, 'activate']);
      if (JVB_TESTING) {
         $this->cache->flush();
      }
        add_action('render_block', [$this, 'renderBlock'], 99, 3);
      jvb_register_do_once('buildDirectories', [$this, 'activate']);
      add_action('init', [$this, 'registerDirectories']);
        add_action('render_block', [$this, 'renderBlock'], 998, 3);
    }
    public function registerDirectories()
    public function registerDirectories():void
    {
        $plural = 'Directories';
        $singular = 'Directory';
        register_post_type(BASE.'directory', array(
            'labels'                => [
                'name' => $plural,
                'singular_name' => $singular,
                'menu_name' => $plural,
                'add_new' => "Add New {$singular}",
                'add_new_item' => "Add New {$singular}",
                'edit_item' => "Edit {$singular}",
                'new_item' => "New {$singular}",
                'view_item' => "View {$singular}",
                'search_items' => "Search {$plural}",
                'not_found' => "No {$plural} found",
                'not_found_in_trash' => "No {$plural} found in Trash"
            ],
            'menu_icon'             => jvbCSSIcon('asc'),
            'public'                => true,
            'publicly_queryable'    => true,
            'show_in_menu'          => true,
            'show_in_admin_bar'     => false,
            'has_archive'           => true,
            'rewrite'               => array(
                'slug'          => 'directory',
                'with_front'    => false
            ),
            'capability_type'   => 'post',
            'supports'          => array('title', 'editor', 'content', 'excerpt', 'custom-fields')
        ));
      $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'          => true,
         '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 activate()
   public function getDirectories():array
   {
      $directories = get_option(BASE.'directories');
      if (!$directories) {
         $directories = [];
         //content
         $content = Registrar::getFeatured('show_directory', 'post');
         if(!empty($content)) {
            foreach ($content as $key) {
               $directories[$key] = 'content';
            }
         }
         $taxonomies = Registrar::getFeatured('show_directory', 'term');
         if(!empty($taxonomies)) {
            foreach ($taxonomies as $key) {
               $directories[$key] = 'taxonomy';
            }
         }
         $users = Registrar::getFeatured('show_directory', 'user');
         if(!empty($users)) {
            foreach ($users as $key) {
               $directories[$key] = 'user';
            }
         }
         update_option(BASE.'directories', $directories);
      }
      return $directories;
   }
    public function activate():void
    {
//    $tmp = new self();
//    $this->registerDirectories();
        $created = [];
        $directories = [];
      $this->getDirectories();
      foreach($this->directories as $directory => $type) {
         $registrar = Registrar::getInstance($directory);
         if (!$registrar){
            error_log('Could not find registrar for making directory for '.$directory);
            continue;
         }
        foreach (jvbGlobalDirectories() as $directory => $type) {
            switch ($type) {
                case 'content':
                    $config = JVB_CONTENT;
                    break;
                case 'tax':
                    $config = JVB_TAXONOMY;
                    break;
                case 'user':
                    $config = JVB_USER;
                    break;
            }
            $title = $config[$directory]['directory']??$config[$directory]['plural'];
            $excerpt = implode(' ', $config[$directory]['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_home_url(null, '/directory/'.$slug),
                    'page'  => $title,
                    'description'    =>$config[$directory]['description']??[],
                    'type'  => $type,
                    'extra'    => $config[$directory]['directory_extra'] ??[],
                ];
            }
         $config = $registrar->getConfig('directory');
         $title = $config['title'];
         $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'] ??[],
            ];
         }
         if ($config['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 (jvbCheck('isGrouped', $config[$directory])) {
                $title = $title. ', but Grouped';
                $excerpt = 'Too many options in the list? This is grouped by type, nested deep, man.';
            $slug = sanitize_title(str_replace(', but', '', $title));
                $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_home_url(null, '/directory/'.$slug),
                        'page'  => $title,
                        'description'    =>$config[$directory]['description']??[],
                        'type'  => $type,
                        'extra'    => $config[$directory]['directory_extra'] ??[],
                    ];
                }
            }
        }
        if (jvbCheck('has_map', JVB_SITE)) {
            $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_home_url(null, '/directory/map'),
                    'page'  => 'Map',
                    'type'  => 'term',
                ];
            }
        }
//        if (Site::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);
@@ -165,20 +216,55 @@
        }
    }
    public static function getConfig(int $ID):array
   protected function buildDirectoryList():array
   {
      $saved = get_option(BASE.'directory_list', []);
      if (empty($saved)) {
         $all = new WP_Query([
            'post_type' => BASE.'directory',
            'post_status'  => 'publish',
            'posts_per_page'  => -1,
         ]);
         foreach($all->posts as $post) {
            $config = Registrar::getInstance($post->post_name)->getConfig('directory')??false;
            $saved[$post->post_name] = [
               'slug'   => $post->post_name,
               'title'  => $post->post_title,
               'ID'  => $post->ID,
               'url' => get_the_permalink($post->ID),
               'page'   => $post->post_title,
               'description'  => ($config) ?$config['description'] :'',
               'type'   => get_post_meta($post->ID, self::$type,true),
               'extra'  => ($config) ?$config['directory_extra'] : [],
            ];
         }
         update_option(BASE.'directory_list', $saved);
         wp_reset_postdata();
      }
      return $saved;
   }
   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 = $this->buildDirectoryList();
      }
      return $this->directoryList;
   }
    public static function getConfig(int $ID):Registrar|false
    {
        $type = get_post_meta($ID, self::$type, true);
        $slug = get_post_meta($ID, self::$slug, true);
        switch ($type) {
            case 'content':
                return JVB_CONTENT[$slug];
            case 'taxonomy':
                return JVB_TAXONOMY[$slug];
            case 'user':
                return JVB_USER[$slug];
        }
        return [];
    }
      return Registrar::getInstance($slug);
   }
    public function letters():array
    {
@@ -212,7 +298,7 @@
        ];
    }
    protected function alphabetizeMe(
    public function alphabetizeMe(
        array $list,
        string $name = '',
        string $url = '',
@@ -239,20 +325,35 @@
        return $list;
    }
   public function directories(string $search = 'all'):array
   {
      $directories = $this->getDirectoryList();
      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 = '<h1>Directory of Directories</h1>
            $cache = '<h1>'.$this->referAs().' of '.$this->referAs(true).'</h1>
                <p>You like lists? We\'ve got \'em!</p>
                <section class="directories item-grid">';
            foreach ($this->directories as $slug => $directory) {
                <ul class="directories">';
            foreach ($this->directoryList as $slug => $directory) {
               $config = Registrar::getInstance($slug);
               $aOpen = '<a href="'.$directory['url'].'" title="See our list of '.$directory['title'].'">';
               $aClose = '</a>';
               $cache .= '<div class="directory col start">
                '.$aOpen.jvbIcon($slug).$aClose.
                  '<h2>'.$aOpen.$directory['title'].$aClose.'</h2>';
               $cache .= '<li class="directory col start">
                  '. $aOpen.jvbIcon($config->getIcon() !== '' ? $config->getIcon() :'list-dashes').$directory['title'].$aClose;
               if (!empty($directory['description'])) {
                  $cache .= '<div class="description">';
                  foreach ($directory['description'] as $description) {
@@ -260,9 +361,9 @@
                  }
                  $cache .= '</div>';
               }
               $cache .= '</div>';
               $cache .= '</li>';
            }
            $cache .= '</section>';
            $cache .= '</ul>';
            return $cache;
         }
      );
@@ -273,11 +374,14 @@
      $cache = $this->cache->remember(
         'index',
         function() {
            $cache = '<nav class="directory"><ul>';
            foreach (jvbDirectories() as $slug => $directory) {
            $cache = '<nav class="directory condensed"><ul>';
            foreach ($this->getDirectoryList() as $slug => $directory) {
               $actualSlug = str_replace('-grouped', '', $slug);
               $config = Registrar::getInstance($actualSlug);
               $icon = $config->getIcon() !== '' ? jvbIcon($config->getIcon()) : '';
               $cache .= '<li id="'.$slug.'">
                    <a href="'.$directory['url'].'">'.
                  jvbIcon(str_replace('-grouped', '', $slug)).$directory['title'].'
                    <a href="'.$directory['url'].'" class="'.$actualSlug.'">'.
                  $icon.$directory['title'].'
                    </a>
                </li>';
            }
@@ -285,90 +389,119 @@
            return $cache;
         }
      );
        if ($current !== '' && array_key_exists($current, jvbDirectories())) {
        if ($current !== '' && array_key_exists($current, $this->directories())) {
            $open = ($open) ? ' open' : '';
            $cache = '<details'.$open.'><summary class="row btw">Other Directories:</summary>'.
            $cache = '<details'.$open.'><summary class="row btw">Other '.$this->referAs(true).':</summary>'.
                     str_replace('id="'.$current.'"', 'id="'.$current.'" class="current"', $cache)
                     .'</details>';
        }
        return $cache;
    }
    private function renderDirectory():string
    {
private function renderDirectory(): string
{
   $slug = get_post_meta(get_the_ID(), self::$slug, true);
   if ($slug === '') {
      return '';
   }
        $slug = get_post_meta(get_the_ID(), self::$slug, true);
        if ($slug === '') {
            return '';
        }
      return $this->cache->remember(
         $slug,
         function() use ($slug) {
            $out = '<h1>'.$this->directories[$slug]['title'].'</h1>';
            $out .= '<div class="description">';
   $type = $this->directories[$slug];
   $registrar = Registrar::getInstance($slug);
   $config = $registrar->getConfig('directory');
            foreach ($this->directories[$slug]['description']??[] as $p) {
               $out .= '<p>'.$p.'</p>';
            }
            $out .= '</div>';
            $out .= $this->renderIndex($slug);
   $paged = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
            $data = $this->directories[$slug];
            $list = [];
            switch ($data['type']) {
               case 'content':
                  $get = new WP_Query([
                     'post_type'            => jvbCheckBase($slug),
                     'posts_per_page'    => -1,
                     'orderby'            => 'title',
                     'order'                => 'ASC'
                  ]);
   $cacheKey = $slug . '_page_' . $paged;
   return $this->cache->remember(
      $cacheKey,
      function() use ($slug, $type, $registrar, $config, $paged) {
         $out = '<h1>' . $this->directoryTitle($registrar) . '</h1>';
         $out .= '<div class="description">';
         foreach ($config['description'] ?? [] as $p) {
            $out .= '<p>' . $p . '</p>';
         }
         $out .= '</div>';
         $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 ($registrar->hasFeature('is_timeline')) {
                  $args['post_parent'] = 0;
               }
               $get = new WP_Query($args);
                  $hasExtra = $registrar->hasFeature('directory_extra');
                  if ($get->have_posts()) {
                     while ( $get->have_posts() ) {
                        $get->the_post();
                        $extra = [];
                        foreach ( $data['extra'] as $item ) {
                           $item = jvbCheckBase( $item );
                        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,
                              ];
                              $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 ) : html_entity_decode($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
                           );
                        }
                        $list = $this->alphabetizeMe(
                           $list,
                           get_the_title(),
                           get_the_permalink(),
                           get_the_ID(),
                           $extra
                        );
                     }
                  }
                  wp_reset_postdata();
                  break;
               case 'tax':
               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',
                     '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 = false;
                     $extra = [];
                     foreach ($get as $term) {
                        $list = $this->alphabetizeMe(
                           $list,
                           $term->name,
                           html_entity_decode($term->name),
                           get_term_link( $term->term_id, jvbCheckBase( $slug ) ),
                           $term->term_id,
                           $extra
@@ -384,17 +517,20 @@
                  ]);
                  break;
               default:
                  $list = [];
                  break;
            }
            $out .= '<section class="directory-list '.$slug.'">';
            if (empty($list)) {
               $out .= '<h2>Nothing here.</h2><p>We don\'t have anything here yet.</p>';
            } else {
               $out .= $this->renderLettersIndex($list);
               $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 .= '</section>';
@@ -412,7 +548,7 @@
        }
      return $this->cache->remember(
         $slug.'_group',
         function() {
         function() use ($slug){
            $out = '<h1>'.$this->directories[$slug]['title'].'</h1>';
            $out .= '<div class="description">';
@@ -456,11 +592,11 @@
         $children =$this->renderListChunk($taxonomy, $term->term_id);
         $out .= '<li>';
         if ($children !== '') {
            $out .= '<details class="term"><summary class="row btw"><a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="See more '.$term->name.'">'.$term->name.'</a></summary>';
            $out .= '<details class="term"><summary class="row btw"><a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="See more '.html_entity_decode($term->name).'">'.$term->name.'</a></summary>';
            $out .= $children;
            $out .= '</details>';
         } else {
            $out .= '<a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="See more '.$term->name.'">'.$term->name.'</a>';
            $out .= '<a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="See more '.$term->name.'">'.html_entity_decode($term->name).'</a>';
         }
         $out .= '</li>';
        }
@@ -468,23 +604,41 @@
        return $out;
    }
    public function renderLettersIndex(array $list):string
    {
        $out = '<nav class="letters on-this-page"><ul>';
        foreach ($this->letters() as $l) {
            $aOpen = $aClose = $class = '';
            if (array_key_exists($l, $list)) {
                $aOpen = '<a href="#starts-with-'.$l.'">';
                $aClose = '</a>';
                $class = ' class="has"';
            }
            $out .= '<li'.$class.'>'.$aOpen.strtoupper($l).$aClose.'</li>';
        }
   public function renderLettersIndex(array $list, string $type = '', string $slug = ''): string
   {
      $letters_on_page = array_keys($list);
      $letterPageMap = [];
        $out .= '</ul></nav>';
      if ($type !== '' && $slug !== '') {
         $letterPageMap = $this->getLetterPageMap($type, $slug);
      }
        return $out;
    }
      $out = '<nav class="letters on-this-page"><ul>';
      foreach ($this->letters() as $l) {
         $aOpen = $aClose = $class = '';
         if (array_key_exists($l, $list)) {
            // Letter is on current page - link to anchor
            $aOpen = '<a href="#starts-with-' . $l . '">';
            $aClose = '</a>';
            $class = ' class="has current-page"';
         } elseif (isset($letterPageMap[$l])) {
            // Letter exists but on different page - link to that page with GET param
            $page = $letterPageMap[$l]['page'];
            $url = add_query_arg('page', $page, get_permalink()) . '#starts-with-' . $l;
            $aOpen = '<a href="' . $url . '" title="Go to page ' . $page . '">';
            $aClose = '</a>';
            $class = ' class="has other-page"';
         }
         $out .= '<li' . $class . '>' . $aOpen . strtoupper($l) . $aClose . '</li>';
      }
      $out .= '</ul></nav>';
      return $out;
   }
    public function renderLettersList(array $list, string $type):string
    {
@@ -493,7 +647,7 @@
        $out = '<ul class="list-none">';
        foreach ($list as $letter => $items) {
            $out .= '<li id="starts-with-'.$letter.'" class="row a-start btw"><h3>'.strtoupper($letter).'</h3><ul>';
            $out .= '<li id="starts-with-'.$letter.'" class="row a-start btw nowrap"><h3>'.strtoupper($letter).'</h3><ul>';
            foreach ($items as $item) {
                $extra = '';
                if (!empty($item['extra'])) {
@@ -504,10 +658,16 @@
                    }
                    $extra .= '</span>';
                }
                $out .= '<li class="row btw">
            $item_html = apply_filters('jvb_directory_render_item', '', $item, $type, $extra);
            if (empty($item_html)) {
               $item_html = '<li class="row btw">
                    <a href="'.$item['url'].'" title="More about '.$item['name'].'">
                        '.$item['name'].'</a>'.$extra.
                '</li>';
                        '.$item['name'].'</a>'.$extra.'
                </li>';
            }
            $out .= $item_html;
            }
            $out .= '</ul></li>';
@@ -525,15 +685,14 @@
            return $content;
        }
      error_log('Still working on directory manager...');
        // For archive page
        if (is_post_type_archive(BASE.'directory') && $block['blockName'] === 'core/group') {
            return ($block['attrs']['tagName']??false === 'main') ? '<main>'.$this->renderArchive().'</main>' : $content;
            return ($block['attrs']['tagName']??'' === 'main') ? '<main>'.$this->renderArchive().'</main>' : $content;
        }
        // For single directory posts
        if ($block['blockName'] === 'core/group') {
            switch (get_post_meta(get_the_ID(), BASE.'grouped_directory', true)) {
                case '':
                    return '<main>' . $this->renderDirectory() . '</main>';
@@ -544,16 +703,203 @@
        return $content;
    }
   protected function directoryTitle(Registrar $registrar):string
   {
      $config = $registrar->getConfig('directory');
      return $config['title']?: $registrar->getPlural();
   }
   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 = '<nav class="directory-pagination" aria-label="Directory pagination"><ul class="pagination">';
      foreach ($pagination as $page) {
         $out .= '<li>' . $page . '</li>';
      }
      $out .= '</ul></nav>';
      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 = '<nav class="directory-pagination" aria-label="Directory pagination"><ul class="pagination">';
      foreach ($pagination as $page) {
         $out .= '<li>' . $page . '</li>';
      }
      $out .= '</ul></nav>';
      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 = [];
            $registrar = Registrar::getInstance($slug);
            switch ($type) {
               case 'content':
                  global $wpdb;
                  $post_type = jvbCheckBase($slug);
                  $where = $wpdb->prepare("post_type = %s AND post_status = 'publish'", $post_type);
                  if ($registrar && $registrar->hasFeature('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;
   }
}
new DirectoryManager();
function jvbDirectoryConfig():array
{
    $ID = get_the_ID();
    if ($ID) {
        return DirectoryManager::getConfig($ID);
    }
    return [];
}