Jake Vanderwerf
2026-01-01 2bb9aaaf24b794b528e3894ee9f9c42ca6d7fe93
inc/managers/DashboardManager.php
@@ -4,6 +4,7 @@
use JVBase\forms\TaxonomySelector;use JVBase\managers\CRUD;
use JVBase\meta\MetaManager;
use JVBase\utility\Features;
use JVBase\ui\Navigation;
use WP_User;
if (!defined('ABSPATH')) {
@@ -18,28 +19,43 @@
    protected WP_User $user;
    protected CacheManager $cache;
    protected string $role;
   protected string $baseURL;
    protected int $userLink;
    public function __construct()
    {
        $this->cache = CacheManager::for('dashboard', WEEK_IN_SECONDS);
      $this->cache->invalidate();
        add_action('init', [$this, 'registerDashboard']);
        if (!$this->isRegistered()) {
            add_action('init', [$this, 'buildDashboard']);
        }
        $this->user = wp_get_current_user();
        $this->role = jvbUserRole();
        $this->role = jvbUserRole($this->user->ID);
        $this->userLink = (int)get_user_meta($this->user->ID, BASE.'link', true);
      $this->baseURL = get_home_url(null, '/dash');
      add_action('template_redirect', [$this, 'handleRedirects']);
        add_action('template_include', [$this, 'dashboardTemplates']);
        add_action('admin_init', [$this, 'redirectFromAdmin']);
        add_action('wp_enqueue_scripts', [$this, 'dashboardScripts'], 50);
      add_filter('jvbDashboardPage', [$this, 'renderIndex'], 10, 2);
      add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeDashboard'], 10, 1);
    }
   public function excludeDashboard(array $ids):array {
      $cached = $this->cache->remember(
         'dashboardIDs',
         function() {
            return get_posts([
               'post_type' => BASE.'dash',
               'posts_per_page' => -1,
               'fields' => 'ids',
            ]);
         });
      return array_merge($ids, $cached);
   }
    /**
     * Registers the custom post type that handles the dashboard
     * @return void
@@ -170,13 +186,13 @@
      // For valid dashboard pages, check access permissions
      if (!is_404()) {
         $page = $this->getCurrentPage();
         $page = $this->getCurrentPageSlug();
         // Dashboard home is always accessible (if authenticated)
         if ($page === '' || $page === 'dash') {
            return;
         }
         $page = $this->getCurrentPageTitle();
         // Check if page exists in allowed pages
         $allowedPages = $this->getUserAllowedPages();
         if (!in_array($page, $allowedPages)) {
@@ -194,15 +210,16 @@
    public function buildDashboard():void
    {
        $manageableContent = $this->getAllDashboardPages();
        foreach ($manageableContent as $key => $slug) {
         if ($slug === 'dash') {
        foreach ($manageableContent as $slug => $page) {
         if ($page === 'dash') {
            continue;
         }
         $slug = $this->getSlug($slug, $page);
         $existing = get_page_by_path($slug, OBJECT, BASE.'dash');
         if ($existing) {
            continue;
         }
            $title = $this->getTitle($slug);
            $title = $page;
            $ID = wp_insert_post(array(
                'post_title'    => $title,
@@ -306,7 +323,13 @@
        }
        // Get current page/section
        $page = $this->getCurrentPage();
        $page = $this->getCurrentPageTitle();
      $config = $this->getConfig($page);
      if(!empty($config)) {
         add_filter('jvbLoadingIcon', function() use ($config) {
            return $config['icon'];
         });
      }
      $integrationSlugs = array_map(function($name) {
         return sanitize_title(str_replace('_', '-', $name));
      }, array_keys(JVB()->getAvailableServices(false)));
@@ -316,13 +339,14 @@
         // Pass along to the Integrations template handler which knows to check for subpages
         $page = 'integrations';
      }
      echo $this->cache->remember(
         $page,
         function() use ($page) {
            return $this->renderDashboard($page);
         }
      );
      echo $this->renderDashboard($page);
      //TODO: Reenable
//    echo $this->cache->remember(
//       $page,
//       function() use ($page) {
//          return $this->renderDashboard($page);
//       }
//    );
      return '';
    }
@@ -475,41 +499,15 @@
      if (!is_singular(BASE.'dash') && !is_post_type_archive(BASE.'dash')) {
            return;
        }
      wp_enqueue_style('jvb-icons-dash');
      wp_enqueue_style('jvb-icons-forms');
      wp_enqueue_script('jvb-loading');
      wp_enqueue_script('jvb-form');
      // Consolidate all dashboard settings
      wp_localize_script('jvb-loading', 'dashboardSettings', array(
         'loadingMessages' => array(
            'default' => 'Loading...',
            'error' => 'Failed to load page'
         ),
         'strings' => array(
            'deleteConfirm' => 'Are you sure you want to delete this item?',
            'bulkDeleteConfirm' => 'Are you sure you want to delete these items?',
            'deleteSuccess' => 'Item(s) deleted successfully',
            'deleteError' => 'Error deleting item(s)',
            'saveSuccess' => 'Changes saved successfully',
            'saveError' => 'Error saving changes',
            'loadError' => 'Error loading content'
         ),
         'currentUser' => array(
            'id' => $this->user->ID,
            'name' => $this->user->display_name,
            'role' => array_values($this->user->roles)[0] ?? '',
            'type' => str_replace(BASE, '', array_values($this->user->roles)[0]),
            'city' => '', // Add if needed,
            'artistID'  => $this->userLink,
         )
      ));
      wp_enqueue_script('jvb-selector');
      wp_enqueue_script('jvb-uploader');
      wp_enqueue_script('jvb-content');
      wp_enqueue_script('jvb-crud');
      $page = $this->getCurrentPage();
      $page = $this->getCurrentPageSlug();
            switch ($page) {
                case 'notifications':
@@ -546,6 +544,12 @@
                    );
                    }
               break;
            case 'seo':
               wp_enqueue_script('jvb-schema');
               break;
            default:
               wp_enqueue_script('jvb-crud');
               break;
            }
         if (Features::forSite()->has('favourites')) {
             wp_enqueue_script('jvb-favourites');
@@ -569,7 +573,20 @@
         do_action('jvbDashScripts', $page);
    }
   protected function getCurrentPage():string
   protected function getCurrentPageTitle():string
    {
      if (is_post_type_archive(BASE.'dash')) {
         return 'dash';
      }
        global $post;
        if (!$post) {
            return '';
        }
        return $post->post_title;
    }
   protected function getCurrentPageSlug():string
    {
      if (is_post_type_archive(BASE.'dash')) {
         return 'dash';
@@ -582,6 +599,29 @@
        return $post->post_name;
    }
   protected function getIcon(string $slug, string $page):string
   {
      return $this->cache->remember('icon_'.sanitize_title($page), function() use ($slug, $page) {
         $icon = sanitize_title($page);
         if (!is_numeric($slug)) {
            $config = Features::getConfig($slug);
            if (array_key_exists('icon', $config)) {
               $icon = $config['icon'];
            }
         }
         return $icon;
      });
   }
   protected function getSlug(string $slug, string $page):string
   {
      return $this->cache->remember('slug_'.sanitize_title($page), function() use ($slug, $page) {
         if (!is_numeric($slug)) {
            return $slug;
         } else {
            return sanitize_title($page);
         }
      });
   }
    protected function renderHeader():void
    {
@@ -606,16 +646,16 @@
          <link rel="preconnect" href="<?= get_home_url()?>"/>
            <?php wp_head(); ?>
        </head>
    <body class="dashboard<?= ' '.$this->getCurrentPage()?>">
    <body class="dashboard<?= ' '.$this->getCurrentPageSlug()?>">
        <?php jvbAccessibility();?>
        <header>
            <?php
            $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
            $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
            echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
                    <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
               jvbIcon('light', ['title'=> 'Light Mode']).
               jvbIcon('dark', ['title'=>'Dark Mode']).
                    <input class="theme-switch row" id="theme-switcher" name="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode" aria-label="Toggle dark mode"><span class="slider">'.
               jvbIcon('sun-dim', ['title'=> 'Light Mode']).
               jvbIcon('moon', ['title'=>'Dark Mode']).
               '</span></label>';
            ?>
            <p class="title">
@@ -630,7 +670,7 @@
                  }
               }
               if ($out == '') {
                  $out =jvbIcon('home');
                  $out =jvbIcon('house');
               }
                    ?><?= $out ?>
                </a>
@@ -652,45 +692,138 @@
    {
        ?>
        </section>
      <?php
      $menu = new Navigation('sidebar');
      $menuClasses = ['col', 'a-start', 'nowrap'];
      $itemClasses = ['col'];
      $menu->addClass('col a-start')->hasToggle()->defaultMenuClasses($menuClasses);
      $menu->defaultItemClasses($itemClasses);
      $pages = $this->getUserAllowedPages()?:[];
      //Dashboard
         //Referrals
      $dashboard = $menu->addItem('Dashboard',jvbDashIcon('door'))
         ->url($this->baseURL);
//       ->submenu('dashboard')
//       ->defaultMenuClasses($menuClasses)
//       ->defaultItemClasses($itemClasses);
      //notifications
      if (in_array('Notifications', $pages)) {
         $menu->addItem('Notifications',jvbDashIcon('bell'))
            ->url($this->baseURL.'/notifications');
      }
      if (in_array('Referrals', $pages)) {
         $menu->addItem('Referrals', jvbDashIcon('hand-heart'))
            ->url($this->baseURL.'/referrals');
      }
      if (in_array('Favourites', $pages)) {
         $menu->addItem('Favourites', jvbDashIcon('heart'))
            ->url($this->baseURL.'/favourites');
      }
      //Content
         //content types
            //Taxonomies
      $availableContent = array_filter($pages, function($page, $key) {
         return !is_numeric($key) && array_key_exists($key, JVB_CONTENT);
      }, ARRAY_FILTER_USE_BOTH);
      if (!empty ($availableContent)){
         $content = $menu->addItem('Your Content', jvbDashIcon('book-bookmark'))
            ->submenu('content')
            ->defaultMenuClasses($menuClasses)
            ->defaultItemClasses($itemClasses);
         foreach ($availableContent as $slug => $page) {
            $config = JVB_CONTENT[$slug];
            $item = $content->addItem($page, jvbDashIcon($config['icon']))
               ->url($this->baseURL.'/'.$slug);
            $taxonomies = array_filter(JVB_TAXONOMY, function ($value, $key) use ($slug) {
               return in_array($slug, $value['for_content']);
            },1);
            if (!empty ($taxonomies)) {
               //TODO: If we add a dedicated 'create item' page, remove this from the empty check
               $itemMenu = $item->submenu($slug);
               foreach ($taxonomies as $s => $config) {
                  $itemMenu->addItem($config['plural'], $config['icon'])
                     ->url($this->baseURL.'/'.$s);
               }
            }
         }
      }
      //Settings
      $settings = $menu->addItem('Settings', jvbDashIcon('faders'))
         ->submenu('settings')
         ->defaultItemClasses($itemClasses)
         ->defaultMenuClasses($menuClasses);
         //SEO
         if (in_array('SEO', $pages)) {
            $settings->addItem('SEO', jvbDashIcon('robot'))
               ->url($this->baseURL.'/seo');
         }
         //Integrations
         if (in_array('Integrations', $pages)) {
            $settings->addItem('Integrations', jvbDashIcon('plugs-connected'))
               ->url($this->baseURL.'/integrations');
         }
      //Account
      $account = $menu->addItem('Account', jvbDashIcon('user-circle'))
         ->url($this->baseURL.'/account')
         ->submenu('account')
         ->defaultMenuClasses($menuClasses)
         ->defaultItemClasses($itemClasses);
      $account->addItem('Reset Password', jvbDashIcon('password'))
         ->url($this->baseURL.'/reset-password');
         //name + contact
         //reset password
         if (in_array('notifications', $pages)) {
            $account->addItem('Permissions', jvbDashIcon('keyhole'))
               ->url($this->baseURL.'/permissions');
         }
      echo $menu->render();
       ?>
        <footer class="col">
         <?= jvbLoadingScreen() ?>
         <?= TaxonomySelector::outputSelectorModal() ?>
            <nav class="dashboard-nav">
<!--            <nav class="dashboard-nav">-->
                <?php
                $current_page = $this->getCurrentPage();
                $pages = $this->getUserAllowedPages()?:[];
            error_log('PageS: '.print_r($pages, true));
                echo '<ul>';
                foreach ($pages as $page) {
                    // Add data-page attribute for the navigator
                    $active = ($current_page == $page) ? ' class="current"' : '';
                    $current = ($current_page == $page) ? ' aria-current="page"' : '';
               $config = $this->getConfig($page);
               $icon = $config['icon']??$page;
               $title = ucwords(str_replace('-', ' ', $page));
               $link = ($page === 'dash') ? '/'.$page : "/dash/$page";
                    printf(
                        '<li%s><a href="%s"%s data-page="%s" data-dash title="%s">%s<span>%s</span></a></li>',
                        $active,
                        get_home_url(null, $link),
                        $current,
                        $page,
                        $title,
                        jvbIcon($icon, ['title'=> $title]),
                        $title
                    );
                }
                echo '</ul>';
//                $current_page = $this->getCurrentPageSlug();
//                $pages = $this->getUserAllowedPages()?:[];
//                echo '<ul>';
//                foreach ($pages as $slug => $page) {
//             $slug = $this->getSlug($slug, $page);
//             $icon = $this->getIcon($slug, $page);
//             // Add data-page attribute for the navigator
//                    $active = ($current_page == $slug) ? ' class="current"' : '';
//                    $current = ($current_page == $slug) ? ' aria-current="page"' : '';
//
//
//             $link = ($page === 'dash') ? '/'.$page : "/dash/$slug";
//                    printf(
//                        '<li%s><a href="%s"%s data-page="%s" data-dash title="%s">%s<span>%s</span></a></li>',
//                        $active,
//                        get_home_url(null, $link),
//                        $current,
//                        $slug,
//                        $page,
//                        jvbDashIcon($icon, ['title'=> $page]),
//                        $page
//                    );
//                }
//
//                echo '</ul>';
                ?>
            </nav>
<!--            </nav>-->
        </footer>
      <?php
        do_action('jvbRenderDashboardSettings', $this->getCurrentPage());
        do_action('jvbRenderDashboardSettings', $this->getCurrentPageSlug());
      ?>
        <?php wp_footer(); ?>
@@ -705,6 +838,14 @@
      if ($page !== '' && $page !== 'dash') {
         return $content;
      }
      if (Features::forSite()->has('referrals')) {
         $whatever = JVB()->referrals()->getReferralWelcomeMessage($this->user->ID);
         if (!empty($whatever)) {
            return $whatever;
         }
      }
      ob_start();
        $name = ($this->user->first_name !== '') ? $this->user->first_name : $this->user->display_name;
@@ -713,7 +854,6 @@
        $pages = $this->getUserAllowedPages();
        echo '<h2>What would you like to do today?</h2>';
        echo '<ul>';
@@ -722,18 +862,14 @@
            continue;
         }
            $title = $this->getTitle($page);
            $url = sanitize_title($title);
            $description = $this->getDescription($page);
         $icon = $page;
         if (!is_numeric($slug)) {
            $config = Features::getConfig($slug);
            if (array_key_exists('icon', $config)) {
               $icon = $config['icon'];
            }
         }
         $slug = $this->getSlug($slug, $page);
         $icon = $this->getIcon($slug, $page);
            if ($title !== '') {
                echo '<li><p><a href="'.get_home_url(null, '/dash/'.$url.'/').'"
                    data-page="'.$url.'" data-dash>'.jvbIcon($icon).ucwords($title).'</a></p></li>';
                echo '<li><p><a href="'.get_home_url(null, '/dash/'.$slug.'/').'"
                    data-page="'.$slug.'" data-dash>'.jvbDashIcon($icon).ucwords($title).'</a></p></li>';
            }
        }
@@ -781,7 +917,7 @@
      if ($content !== '') {
         echo $content;
      } else {
      jvbRenderSections($this->user->ID, 'user', jvbUserRole());
         jvbRenderSections($this->user->ID, 'user', jvbUserRole());
      }
    }
@@ -794,13 +930,13 @@
         $out = '<nav class="integrations"><ul>';
         $url = get_home_url(null, '/dash/integrations/');
         $out .= '<li><a href="'.$url.'">'.jvbIcon('plugs-connected').'Integrations</a></li>';
         $out .= '<li><a href="'.$url.'">'.jvbDashIcon('plugs-connected').'Integrations</a></li>';
         foreach ($integrations as $name=> $integration) {
            if (!JVB()->userCanConnect($name, $this->user->ID) || !$integration->hasDefaults()) {
               continue;
            }
            $link = sanitize_title(str_replace('_', '-',$name));
            $out .= '<li><a href="'.$url.$link.'">'.jvbIcon($integration->icon).$integration->getTitle().'</a></li>';
            $out .= '<li><a href="'.$url.$link.'">'.jvbDashIcon($integration->icon).$integration->getTitle().'</a></li>';
         }
         $out .= '</ul></nav>';
      }
@@ -851,16 +987,16 @@
        <div class="approvals container">
            <nav class="tabs row start" role="tablist">
                <button type="button" class="tab active" data-tab="summary" role="tab" aria-selected="true">
                    <h2><?= jvbIcon('all')?>All</h2>
                    <h2><?= jvbDashIcon('infinity')?>All</h2>
                </button>
                <button type="button" class="tab" data-tab="artists" role="tab" aria-selected="false">
                    <h2><?= jvbIcon('artists')?>Artists</h2>
                    <h2><?= jvbDashIcon('users-three')?>Artists</h2>
                </button>
                <button type="button" class="tab" data-tab="terms" role="tab" aria-selected="false">
                    <h2><?= jvbIcon('style')?>Terms</h2>
                    <h2><?= jvbDashIcon('hash')?>Terms</h2>
                </button>
                <button type="button" class="tab" data-tab="yours" role="tab" aria-selected="false">
                    <h2><?= jvbIcon('artist')?>Yours</h2>
                    <h2><?= jvbDashIcon('user')?>Yours</h2>
                </button>
            </nav>
        </div>
@@ -937,7 +1073,7 @@
            $active = ($i === 1) ? ' active' : '';
            ?>
            <button type="button" class="tab<?=$active?>" data-tab="<?=$type?>" role="tab" aria-selected="<?= ($active !== '') ? 'true' : 'false'?>">
                <h2><?=jvbIcon($settings['icon']??$key)?> <?= $settings['plural'] ?></h2>
                <h2><?=jvbDashIcon($settings['icon']??$key)?> <?= $settings['plural'] ?></h2>
            </button>
            <?php
            $i++;
@@ -964,8 +1100,8 @@
            'vertical',
            'TAB NAV:',
            '',
            jvbIcon('down'),
            jvbIcon('right'))?>
            jvbDashIcon('caret-double-down'),
            jvbDashIcon('caret-double-right'))?>
    </div>
    <div class="items-container">
@@ -1024,18 +1160,18 @@
        <template class="<?= $type ?>Row">
            <tr>
                <td>
                     <?= jvbIcon('grab') ?>
                     <?= jvbDashIcon('dots-six-vertical') ?>
                 </td>
                 <td data-id="actions" class="col">
                     <?= jvbRenderToggleTextField(
                         'public',
                         '',
                         '',
                         jvbIcon('show'),
                         jvbIcon('hide'))
                         jvbDashIcon('eye'),
                         jvbDashIcon('eye-closed'))
                     ?>
                     <button type="button" data-action="edit">
                         <?= jvbIcon('edit') ?>
                         <?= jvbDashIcon('pencil-simple') ?>
                    </button>
                </td>
                <?php
@@ -1094,55 +1230,55 @@
      $pages = $this->cache->get($cacheKey);
      if ($pages === false || JVB_TESTING) {
         $pages = [];
         $pages[] = 'SEO';
         // Add feature-dependent pages (non-config)
         if (Features::forSite()->has('referrals')) {
            $pages[] = 'referrals';
            $pages[] = 'Referrals';
         }
         if (Features::forMembership()->has('can_invite')) {
            $pages[] = 'invites';
            $pages[] = 'Invites';
         }
         if (Features::forMembership()->has('term_approval')) {
            $pages[] = 'approvals';
            $pages[] = 'Approvals';
         }
         if (Features::forMembership()->has('forum')) {
            $pages[] = 'news';
            $pages[] = 'News';
         }
         if (Features::forMembership()->has('member_content')) {
            $pages[] = 'metrics';
            $pages[] = 'Metrics';
         }
         if (Features::forSite()->has('favourites')) {
            $pages[] = 'favourites';
            $pages[] = 'Favourites';
         }
         if (Features::anyContentHas('karma') || Features::anyTaxonomyHas('karma') || Features::anyUserHas('karma')) {
            $pages[] = 'karmic-score';
            $pages[] = 'Karmic Score';
         }
         if (Features::forSite()->has('notifications')) {
            $pages[] = 'notifications';
            $pages[] = 'Notifications';
         }
         if (Features::forSite()->has('support')) {
            $pages[] = 'support';
            $pages[] = 'Support';
         }
         if (Features::hasAnyIntegration()) {
            $pages[] = 'integrations';
            $pages[] = 'Integrations';
         }
         // Add all content types (with config keys)
         foreach (JVB_CONTENT as $slug => $config) {
            $pages[$slug] = sanitize_title($config['plural']);
            $pages[$slug] = $config['plural'];
         }
         foreach (JVB_TAXONOMY as $slug=>$config) {
            $pages[$slug] = sanitize_title($config['plural']);
            $pages[$slug] = $config['plural'];
         }
         // Allow filtering
@@ -1195,9 +1331,10 @@
         return [];
      }
      $cacheKey = "user_pages_{$userID}";
      $pages = $this->cache->get($cacheKey);
      $pages = false;
      if ($pages === false || JVB_TESTING) {
         if (user_can($userID, 'manage_options')) {
            // Admin gets all pages as flat array
@@ -1220,7 +1357,7 @@
               }
               switch ($type) {
                  case 'content':
                     if (!user_can($userID, "edit_{$permission}")) {
                     if (user_can($userID, "edit_{$permission}")) {
                        $remove = false;
                     }
                     break;
@@ -1228,12 +1365,14 @@
                     $config = Features::getConfig($key, 'taxonomy');
                     if (array_key_exists('is_content', $config) && $config['is_content'] && (user_can($userID, "own_{$key}") || user_can($userID, "manage_{$key}"))) {
                        $remove = false;
                     } else if (count(array_intersect($config['for_content'], array_keys($pages))) > 0) {
                        $remove = false;
                     }
                     break;
               }
            } else {
               switch ($slug) {
                  case 'integrations':
                  case 'Integrations':
                     foreach($roles as $role) {
                        if (Features::hasAnyIntegration('user', $role)) {
                           $remove = false;
@@ -1253,7 +1392,7 @@
                        }
                     }
                     break;
                  case 'approvals':
                  case 'Approvals':
                     $canApprove = false;
                     if (Features::forMembership()->has('term_approval')) {
                        if (array_key_exists('can_approve', JVB_MEMBERSHIP)) {
@@ -1303,6 +1442,8 @@
                        }
                     }
                     break;
                  case 'dash':
                  case 'Referrals':
                  case 'favourites':
                  case 'notifications':
                  case 'support':
@@ -1311,9 +1452,9 @@
                  default:
                     break;
               }
               if ($remove) {
                  unset($pages[$key]);
               }
            }
            if ($remove) {
               unset($pages[$key]);
            }
         }