From 747d741293e064a979d7bf6c143ef969ea6d7629 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 24 May 2026 20:49:44 +0000
Subject: [PATCH] =GMBReview block minor tweaks. Refactored ReferralManager.php and ReferralRoutes.php to utilize the manager for all logic, and CustomTable for table interactions.

---
 inc/managers/DirectoryManager.php |  857 ++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 611 insertions(+), 246 deletions(-)

diff --git a/inc/managers/DirectoryManager.php b/inc/managers/DirectoryManager.php
index f2418cc..a4ebc71 100644
--- a/inc/managers/DirectoryManager.php
+++ b/inc/managers/DirectoryManager.php
@@ -5,158 +5,230 @@
 	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_filter('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 = Site::getDirectorySingular()??'Directory';
+		$plural = Site::getDirectoryPlural()??'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(2, '/directory/'.$slug),
-                    'page'  => $title,
-                    'description'    =>$config[$directory]['description']??[],
-                    'type'  => $type,
-                    'extra'    => $config[$directory]['directory_extra'] ??[],
-                ];
-            }
+			$config = $registrar->getConfig('directory');
+			$title = $config['title'];
+			//Bail early if we've already created the page
+			$existing = new WP_Query([
+				'post_type'	=> BASE.'directory',
+				'name'	=> sanitize_title($title),
+				'posts_per_page'	=> 1,
+			]);
+			if ($existing->have_posts()) {
+				$existing = $existing->posts[0];
+				$created[$directory] = $existing->ID;
+				$directories[$directory] = [
+					'slug'	=> $existing->post_name,
+					'title'	=> $existing->post_title,
+					'ID'	=> $existing->ID,
+					'url'	=> get_the_permalink($existing->ID),
+					'page'	=> $existing->post_title,
+					'description'=> $existing->post_excerpt,
+					'type'	=> $type,
+					'extra'	=> $config[$directory]['directory_extra']??[],
+				];
+				continue;
+			}
 
+			$excerpt = implode(' ', $config['description']??[]);
+			$ID = wp_insert_post([
+				'post_type'		=> BASE.'directory',
+				'post_title'	=> $title,
+				'post_status'	=> 'publish',
+				'post_excerpt'	=> $excerpt,
+				'post_name'			=> 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(2, '/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(2, '/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 +237,56 @@
         }
     }
 
-    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 +320,7 @@
         ];
     }
 
-    protected function alphabetizeMe(
+    public function alphabetizeMe(
         array $list,
         string $name = '',
         string $url = '',
@@ -239,20 +347,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 left">
+						'. $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 +383,9 @@
 						}
 						$cache .= '</div>';
 					}
-					$cache .= '</div>';
+					$cache .= '</li>';
 				}
-				$cache .= '</section>';
+				$cache .= '</ul>';
 				return $cache;
 			}
 		);
@@ -273,11 +396,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 +411,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 x-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 +539,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 +570,7 @@
         }
 		return $this->cache->remember(
 			$slug.'_group',
-			function() {
+			function() use ($slug){
 				$out = '<h1>'.$this->directories[$slug]['title'].'</h1>';
 				$out .= '<div class="description">';
 
@@ -456,11 +614,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 x-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 +626,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 +669,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 top x-btw nowrap"><h3>'.strtoupper($letter).'</h3><ul>';
             foreach ($items as $item) {
                 $extra = '';
                 if (!empty($item['extra'])) {
@@ -504,10 +680,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 x-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 +707,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 +725,200 @@
 
         return $content;
     }
+
+	protected function directoryTitle(Registrar $registrar):string
+	{
+		$config = $registrar->getConfig('directory');
+		return $config['title']?: $registrar->getPlural();
+	}
+
+	public function referAs($plural = false):string
+	{
+		return ($plural) ? Site::getDirectoryPlural()??'Directories' : Site::getDirectorySingular()??'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 [];
-}
-

--
Gitblit v1.10.0