From 235ce5716edc2f7cbe80fdccf26eac7269587839 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 08 Jun 2026 04:38:18 +0000
Subject: [PATCH] =FavouritesManager.php and FavouritesRoutes.php fixes. Moving all logic to FavouritesManager.php. Still some left to do

---
 JVBase.php                              |    5 
 inc/managers/CRUDManager.php            |    6 
 inc/managers/CustomTable.php            |   49 +++-
 assets/js/concise/FrontendFavourites.js |   40 +++
 inc/ui/CRUDSkeleton.php                 |   10 
 assets/css/dash.min.css                 |    2 
 assets/js/min/favourites.min.js         |    2 
 inc/registrar/Fields.php                |   12 
 inc/rest/routes/FavouritesRoutes.php    |  325 +++++++-------------------------
 inc/managers/FavouritesManager.php      |  109 ++++++++++
 inc/registrar/Registrar.php             |   11 
 11 files changed, 278 insertions(+), 293 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index 28efe56..bf0db8a 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -219,6 +219,11 @@
 		return array_merge(array_keys($this->content), array_keys($this->taxonomies));
 	}
 
+	public function favourites(): FavouritesManager|false
+	{
+		return $this->managers['favourites'] ?? false;
+	}
+
 	public function dashboard(): DashboardManager|false
 	{
 		return $this->managers['dash'] ?? false;
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index 3e6de63..069bff2 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-.replace{margin-left:var(--btn_)!important}.dashboard aside.main.left{bottom:0}.dashboard .qtoggle{left:0;bottom:0;margin:0!important}nav.sidebar{--wrap:nowrap;--align:flex-start;position:fixed;bottom:0;top:var(--btn);z-index:var(--z-4);height:calc(100% - var(--btn));background-color:rgb(var(--base));box-shadow:rgba(var(--base),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto;margin-left:0!important;padding-bottom:var(--btn)}nav.sidebar.left{left:0}nav.sidebar.right{right:0}nav.sidebar .icon{--w:var(--chip_);width:var(--w);transition:var(--trans-size)}nav.sidebar.open{width:fit-content;max-width:85vw}nav.sidebar.open .icon{--w:var(--chip)}nav.sidebar ul{height:max-content;width:100%;--gap:0;--dir:column}nav.sidebar li{--justify:center;--wrap:nowrap;--align:flex-start;overflow:hidden;height:max-content}nav.sidebar ul ul{max-height:0;overflow:hidden;transform:scaleY(0);transform-origin:top;transition:var(--trans-base)}nav.sidebar li.open>ul{max-height:max-content;transform:scaleY(1)}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .title{display:none}nav.sidebar.open .title{display:block;white-space:nowrap}nav.sidebar a{--justify:flex-start}nav.sidebar .a{gap:.5rem;display:flex;width:100%;justify-content:flex-start;align-items:center;min-height:var(--btn);padding:var(--padding)}nav.sidebar .toggle:not(.main){display:none;width:var(--btn);height:var(--chipchip)}nav.sidebar.open .toggle{display:flex}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base),var(--op-45)) var(--shdw)}.all-filters{font-size:var(--txt-x-small)}.all-filters[open]{border:2px solid rgb(var(--action-0));padding:0;border-radius:0 0 var(--radius-outer) var(--radius-outer)}.all-filters summary:hover,.all-filters[open] summary{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast))}.all-filters summary:hover::after{background-color:rgb(var(--action-contrast))}.all-filters summary{width:100%}.all-filters>.row.row{padding:0 .75rem;width:var(--content);position:relative}.all-filters>.row>.label,.all-filters>.row>.row>.label{font-family:var(--heading);font-weight:var(--fw-h-bold);text-transform:uppercase}.all-filters>.row>.label{width:20%}.all-filters>.row>.row>.label{white-space:nowrap}.all-filters .btn+label,.all-filters button{width:var(--chipchip);min-height:var(--chipchip);padding:0}.all-filters .btn+label,.all-filters button{position:unset}.all-filters .row:has(>.btn:not(:checked)+label:hover) :checked+label .label,.all-filters button .label,.btn+label .label{position:absolute;top:2rem;left:1rem;width:max-content;white-space:nowrap;opacity:0;z-index:var(--z-4)}.all-filters .radio-options.order>.row{position:relative;padding-bottom:2rem}.all-filters .radio-options .row .btn+label .label{bottom:0;top:unset;left:0;height:max-content}.all-filters button:hover .label,.btn+label:hover .label,.btn:checked+label .label{opacity:1}.all-filters .radio-options.order>.row{max-width:49%}.all-filters .radio-options.order{margin-top:1rem}.all-filters .filters select,.all-filters .filters>.row{width:max-content}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}section.main-actions.main-actions{padding:0;position:absolute;left:var(--offScreen)}.dashboard main>footer{padding:0;margin:0;position:absolute;left:var(--offScreen)}.item .select-item-label{width:100%;height:100%;aspect-ratio:1}
\ No newline at end of file
+.replace{margin-left:var(--btn_)!important}.dashboard aside.main.left{bottom:0}.dashboard .qtoggle{left:0;bottom:0;margin:0!important}nav.sidebar{--wrap:nowrap;--align:flex-start;position:fixed;bottom:0;top:var(--btn);z-index:var(--z-4);height:calc(100% - var(--btn));background-color:rgb(var(--base));box-shadow:rgba(var(--base),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto;margin-left:0!important;padding-bottom:var(--btn)}nav.sidebar.left{left:0}nav.sidebar.right{right:0}nav.sidebar .icon{--w:var(--chip_);width:var(--w);transition:var(--trans-size)}nav.sidebar.open{width:fit-content;max-width:85vw}nav.sidebar.open .icon{--w:var(--chip)}nav.sidebar ul{height:max-content;width:100%;--gap:0;--dir:column}nav.sidebar li{--justify:center;--wrap:nowrap;--align:flex-start;overflow:hidden;height:max-content}nav.sidebar ul ul{max-height:0;overflow:hidden;transform:scaleY(0);transform-origin:top;transition:var(--trans-base)}nav.sidebar li.open>ul{max-height:max-content;transform:scaleY(1)}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .title{display:none}nav.sidebar.open .title{display:block;white-space:nowrap}nav.sidebar a{--justify:flex-start}nav.sidebar .a{gap:.5rem;display:flex;width:100%;justify-content:flex-start;align-items:center;min-height:var(--btn);padding:var(--padding)}nav.sidebar .toggle:not(.main){display:none;width:var(--btn);height:var(--chipchip)}nav.sidebar.open .toggle{display:flex}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base),var(--op-45)) var(--shdw)}.all-filters{font-size:var(--txt-x-small)}.all-filters[open]{border:2px solid rgb(var(--action-0));padding:0;border-radius:0 0 var(--radius-outer) var(--radius-outer)}.all-filters summary:hover,.all-filters[open] summary{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast))}.all-filters summary:hover::after{background-color:rgb(var(--action-contrast))}.all-filters summary{width:100%}.all-filters>.row.row{padding:0 .75rem;width:var(--content);position:relative}.all-filters>.row>.label,.all-filters>.row>.row>.label{font-family:var(--heading);font-weight:var(--fw-h-bold);text-transform:uppercase}.all-filters>.row>.label{width:20%}.all-filters>.row>.row>.label{white-space:nowrap}.all-filters .btn+label,.all-filters button{width:var(--chipchip);min-height:var(--chipchip);padding:0}.all-filters .btn+label,.all-filters button{position:unset}.all-filters .row:has(>.btn:not(:checked)+label:hover) :checked+label .label,.all-filters button .label,.btn+label .label{position:absolute;top:2rem;left:1rem;width:max-content;white-space:nowrap;opacity:0;z-index:var(--z-4)}.all-filters .radio-options.order>.row{position:relative;padding-bottom:2rem}.all-filters .radio-options .row .btn+label .label{bottom:0;top:unset;left:0;height:max-content}.all-filters button:hover .label,.btn+label:hover .label,.btn:checked+label .label{opacity:1}.all-filters .radio-options.order>.row{max-width:49%}.all-filters .radio-options.order{margin-top:1rem}.all-filters .filters select,.all-filters .filters>.row{width:max-content}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}section.main-actions.main-actions{padding:0;position:absolute;left:var(--offScreen)}.dashboard main>footer{padding:0;margin:0;position:absolute;left:var(--offScreen)}.item .select-item-label{width:100%;height:100%;aspect-ratio:1}dialog form nav.tabs{--wrap:nowrap!important;--gap:0!important;overflow-x:auto;touch-action:pan-x;position:absolute;top:0;left:0;right:0;max-width:100%;height:var(--chipchip);padding:0;background-color:rgba(var(--base),var(--op-5))}dialog form nav.tabs button{min-height:var(--chipchip);padding:0 1rem}dialog nav.tabs button:hover{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast))}dialog:has(nav.tabs) .wrap{padding-top:3rem}dialog{height:100%!important}
\ No newline at end of file
diff --git a/assets/js/concise/FrontendFavourites.js b/assets/js/concise/FrontendFavourites.js
index 661bbfc..47befbe 100644
--- a/assets/js/concise/FrontendFavourites.js
+++ b/assets/js/concise/FrontendFavourites.js
@@ -71,12 +71,40 @@
 		// Update button icon
 		button.innerHTML = jvbSettings.icons[button.classList.contains('favourited') ? 'heart-filled' : 'heart'];
 
-		this.store.setItem(button.dataset.id, {
-			target_id: button.dataset.id,
-			action: action,
-			type: button.dataset.type,
-			artist: button.dataset.artist,
-		});
+		window.debouncer.schedule(
+			`favourite-${button.dataset.id}`,
+			this.postFavourite({
+				user: window.auth.getUser(),
+				target_id: button.dataset.id,
+				action: action,
+				type: button.dataset.type
+			}),
+			100
+		);
+
+	}
+
+	async postFavourite(args) {
+		await window.auth.fetch(
+			`${jvbSettings.api}favourites`,
+			{
+				method: 'POST',
+				headers: {
+					'X-Action-Nonce': window.auth.getNonce('favourites')
+				},
+				body: args
+			}
+		);
+
+		if (args.action === 'add') {
+			await this.store.setItem(button.dataset.id, {
+				target_id: button.dataset.id,
+				action: action,
+				type: button.dataset.type,
+			}).then(()=>{});
+		} else {
+			await this.store.delete(button.dataset.id).then(()=>{});
+		}
 	}
 
 	// async toggleFavourite(itemType, itemId) {
diff --git a/assets/js/min/favourites.min.js b/assets/js/min/favourites.min.js
index 8369985..6fc4b95 100644
--- a/assets/js/min/favourites.min.js
+++ b/assets/js/min/favourites.min.js
@@ -1 +1 @@
-(()=>{class t{constructor(){let t=window.jvbStore.register("favourites",{storeName:"favourites",keyPath:"id",endpoint:"favourites",headers:{"X-Action-Nonce":window.auth.getNonce("favourites")},indexes:[{name:"content",keyPath:"content"},{name:"listId",keyPath:"listId"}],TTL:36e4,showLoading:!1,filters:{user:window.auth.getUser(),content:"all",order:"desc",orderby:"date",page:1,all:!0}});this.store=t.favourites,this.store.subscribe(((t,e)=>{t}))}toggleFavourite(t){if(!window.auth.getUser())return void(window.location.href=jvbSettings.redirect+"&action=register&type=favourites");t.classList.toggle("favourited");const e=t.classList.contains("favourited")?"add":"remove",o=t.classList.contains("favourited")?`Added ${t.dataset.type} to favourites.`:`Removed ${t.dataset.type} from favourites.`;window.jvbA11y.announce(o),t.innerHTML=jvbSettings.icons[t.classList.contains("favourited")?"heart-filled":"heart"],this.store.setItem(t.dataset.id,{target_id:t.dataset.id,action:e,type:t.dataset.type,artist:t.dataset.artist})}isFavourited(t,e){const o=`${this.userId}_${t}_${e}`;return void 0!==this.store.get(o)}}document.addEventListener("DOMContentLoaded",(function(){window.jvbFavourites=!1,window.auth.subscribe((e=>{"auth-loaded"===e&&(window.jvbFavourites=new t)}))})),window.toggleFavourite=function(t){window.jvbFavourites()?window.jvbFavourites.toggleFavourite(t):console.log("No Favourites Loaded")},window.isFavourited=function(t,e){if(window.jvbFavourites())return window.jvbFavourites.isFavourited(t,e);console.log("No Favourites Loaded")}})();
\ No newline at end of file
+(()=>{class t{constructor(){let t=window.jvbStore.register("favourites",{storeName:"favourites",keyPath:"id",endpoint:"favourites",headers:{"X-Action-Nonce":window.auth.getNonce("favourites")},indexes:[{name:"content",keyPath:"content"},{name:"listId",keyPath:"listId"}],TTL:36e4,showLoading:!1,filters:{user:window.auth.getUser(),content:"all",order:"desc",orderby:"date",page:1,all:!0}});this.store=t.favourites,this.store.subscribe(((t,e)=>{t}))}toggleFavourite(t){if(!window.auth.getUser())return void(window.location.href=jvbSettings.redirect+"&action=register&type=favourites");t.classList.toggle("favourited");const e=t.classList.contains("favourited")?"add":"remove",o=t.classList.contains("favourited")?`Added ${t.dataset.type} to favourites.`:`Removed ${t.dataset.type} from favourites.`;window.jvbA11y.announce(o),t.innerHTML=jvbSettings.icons[t.classList.contains("favourited")?"heart-filled":"heart"],window.debouncer.schedule(`favourite-${t.dataset.id}`,this.postFavourite({user:window.auth.getUser(),target_id:t.dataset.id,action:e,type:t.dataset.type}),100)}async postFavourite(t){await window.auth.fetch(`${jvbSettings.api}favourites`,{method:"POST",headers:{"X-Action-Nonce":window.auth.getNonce("favourites")},body:t}),"add"===t.action?await this.store.setItem(button.dataset.id,{target_id:button.dataset.id,action,type:button.dataset.type}).then((()=>{})):await this.store.delete(button.dataset.id).then((()=>{}))}isFavourited(t,e){const o=`${this.userId}_${t}_${e}`;return void 0!==this.store.get(o)}}document.addEventListener("DOMContentLoaded",(function(){window.jvbFavourites=!1,window.auth.subscribe((e=>{"auth-loaded"===e&&(window.jvbFavourites=new t)}))})),window.toggleFavourite=function(t){window.jvbFavourites()?window.jvbFavourites.toggleFavourite(t):console.log("No Favourites Loaded")},window.isFavourited=function(t,e){if(window.jvbFavourites())return window.jvbFavourites.isFavourited(t,e);console.log("No Favourites Loaded")}})();
\ No newline at end of file
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index 58aa17d..1fedd00 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -64,12 +64,10 @@
 		// Fields and sections
 		$this->skeleton->setFields($this->registrar->getFields());
 
-		jvbDump($this->registrar->getSections());
         $sections = $this->registrar->getSections();
         if (count($sections) > 1) {
             foreach ($sections as $config) {
-                jvbDump($config);
-                $this->skeleton->addSection($config['id'], $config);
+                $this->skeleton->addSection($config['slug'], $config);
             }
         }
 
@@ -189,7 +187,7 @@
 
 	protected function addDateRanges():array
 	{
-		return $this->cache->remember(
+		return $this->cache->user()->remember(
 			'dateRanges',
 			function() {
 				$postType = jvbCheckBase($this->content);
diff --git a/inc/managers/CustomTable.php b/inc/managers/CustomTable.php
index 572d8a2..1a6719c 100644
--- a/inc/managers/CustomTable.php
+++ b/inc/managers/CustomTable.php
@@ -319,16 +319,16 @@
 	}
 
 	/**
-	 * Set LIMIT
+	 * Set pagination
 	 *
-	 * @param int $limit Number of records
-	 * @param int $offset Optional offset
+	 * @param int $perPage Number of records per page
+	 * @param int $page Page number (1-based)
 	 * @return self
 	 */
-	public function limit(int $limit, int $offset = 0): self
+	public function limit(int $perPage, int $page = 1): self
 	{
-		$this->builder['limit'] = $limit;
-		$this->builder['offset'] = $offset;
+		$this->builder['per_page'] = $perPage;
+		$this->builder['page'] = $page;
 		return $this;
 	}
 
@@ -601,12 +601,12 @@
 	 *     'where' => ['user_id' => 1],
 	 *     'orderby' => 'date_added',
 	 *     'order' => 'DESC',
-	 *     'limit' => 20
+	 *     'per_page' => 20
 	 * ]);
 	 */
-	public function getMany(array $args = [], string $output = OBJECT): array
+	public function getMany(array $args = [], bool $itemsOnly = true, string $output = OBJECT): array
 	{
-		return $this->cache->remember(
+		$items = $this->cache->remember(
 			$this->cache->generateKey(array_merge($args, ['output' => $output])),
 			function () use ($args, $output) {
 				$query = "SELECT * FROM {$this->fullTableName}";
@@ -628,10 +628,19 @@
 				}
 
 				// LIMIT
-				if (!empty($args['limit'])) {
-					$limit = absint($args['limit']);
-					$offset = !empty($args['offset']) ? absint($args['offset']) : 0;
-					$query .= " LIMIT {$offset}, {$limit}";
+				if (array_key_exists('limit', $args)) {
+					error_log('[CustomTable]::getMany deprecated key \'limit\' - use \'per_page\' instead. '.print_r($args, true));
+					$args['per_page'] = $args['limit'];
+				}
+				if (array_key_exists('offset', $args)) {
+					error_log('[CustomTable]::getMany deprecated key \'offset\' - use \'page\' instead. '.print_r($args, true));
+					$args['page'] = $args['offset'] / $args['limit'];
+				}
+				if (!empty($args['per_page'])) {
+					$perPage = absint($args['per_page']);
+					$page    = !empty($args['page']) ? absint($args['page']) : 1;
+					$offset  = ($page - 1) * $perPage;
+					$query  .= " LIMIT {$offset}, {$perPage}";
 				}
 
 				if (empty($values)) {
@@ -642,8 +651,20 @@
 			}
 		);
 
+		if ($itemsOnly) {
+			return $items;
+		}
+		$page = max(1, $args['page'] ?? 1);
+		$perPage = $args['per_page']??false;
+		$total = $this->count($args['where']);
+		return [
+			'items'		=> $items,
+			'total'		=> $total,
+			'has_more'	=> $perPage && ($page * $perPage) < $total,
+		];
 	}
 
+
 	/**
 	 * Get a specific column value from all matches
 	 * @param string $column
@@ -669,7 +690,7 @@
 					$args['order'] = $order;
 				}
 				if ($limit) {
-					$args['limit'] = $limit;
+					$args['per_page'] = $limit;
 				}
 
 				return array_column($this->getMany($args), $column);
diff --git a/inc/managers/FavouritesManager.php b/inc/managers/FavouritesManager.php
index 0ea911c..125c75a 100644
--- a/inc/managers/FavouritesManager.php
+++ b/inc/managers/FavouritesManager.php
@@ -461,6 +461,115 @@
 				'status'	=> $accept ? 'accepted' : 'rejected',
 			], $args);
 		}
+
+		public function getFavourites(array $args = []):array
+		{
+			return $this->favourites->getMany($args, false);
+		}
+		public function getLists(array $args = []):array
+		{
+			return $this->lists->getMany($args, false);
+		}
+			public function getAvailableLists(array $args = [], bool $include_shared = true):array
+			{
+				$lists = [];
+				$owned = $this->lists->getMany($args, false);
+				foreach ($owned['items'] as &$list) {
+					$list['item_count'] = $this->listItems->count(['list_id' => $list['id']]);
+				}
+
+				$lists['owned'] = $owned;
+
+				if ($include_shared) {
+					$args['where']['status'] = 'accepted';
+					$sharedLists = $this->listShares->getMany($args);
+					$shared = [];
+
+					foreach ($sharedLists as $share) {
+						$sharedList = $this->lists->get(['id' => $share->list_id]);
+						if ($sharedList) {
+							$sharedList['owner_name'] = jvbGetUsername($sharedList['user_id']);
+							$sharedList['item_count'] = $this->listItems->count(['list_id' => $sharedList['id']]);
+							$sharedList['permission_type'] = $sharedList->permission_type;
+							$shared[] = $sharedList;
+						}
+					}
+					$lists['shared'] = $shared;
+				}
+				return $lists;
+			}
+
+			public function userOwnsList(int $list_id, int $user_id):bool
+			{
+				return $this->lists->count(['id' => $list_id, 'user_id' => $user_id]) > 0;
+			}
+
+			public function getListDetails(int $list_id, int $user_id, int $page = 1):array
+			{
+				$list = JVB()->favourites()->getLists(['id' => $list_id, 'user_id' => $user_id]);
+				if (!$list) {
+					return [];
+				}
+				$items = $this->listItems->getMany([
+					'where' 	=> ['list_id' => $list_id],
+					'order_by'	=> 'added_at',
+					'order'		=> 'DESC',
+					'per_page'	=> 25,
+					'page'		=> $page,
+				]);
+
+				$formatted = [];
+
+				foreach($items as $item) {
+					//See if we can get the actual favourite record first
+					if ($item->favourite_id) {
+						$fav = $this->favourites->get(['id' => $item->favourite_id]);
+						if ($fav) {
+							$formatted[] = $fav;
+							continue;
+						}
+					}
+
+					//Create a dummy favourite object otherwise
+					$formatted[] = (object) [
+						'type'		=> $item->item_type,
+						'target_id'	=> $item->item_id,
+						'created_at'=> $item->added_at
+					];
+				}
+
+				$sharedWith = [];
+				$is_owner = $this->userOwnsList($list_id, $user_id);
+				if ($is_owner) {
+					$shares = $this->listShares->getMany([
+						'where' => ['list_id' => $list_id],
+						'order_by' => 'created_at',
+						'order'	=> 'DESC'
+					]);
+					foreach ($shares as $share_item) {
+						$user = [
+							'email'		=> $share_item->email,
+							'status'	=> $share_item->status,
+							'date_added'=> $share_item->created_at,
+						];
+						if ($share_item->status === 'accepted' && $share_item->user_id) {
+							$user['name'] = jvbGetUsername($share_item->user_id);
+							$user['permission_type'] = $share_item->permission_type;
+						}
+						$sharedWith[] = $user;
+					}
+				}
+
+				return [
+					'id'	=> (int)$list['id'],
+					'name'	=> $list['name'],
+					'description'	=> $list['description']??'',
+					'created_at'	=> $list['created_at'],
+					'is_owner'		=> $is_owner,
+					'items'			=> $formatted,
+					'shared_users'	=> $sharedWith
+				];
+			}
 	/***************************************************************
 	 * UTILITY METHODS
 	 **************************************************************/
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index 5635c0c..8e37662 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -97,6 +97,12 @@
 			];
 		}
 		$fields = [
+            'post_status'	=> [
+                'type'		=> 'radio',
+                'label'		=> 'Status',
+                'default'	=> 'draft',
+                'options'	=> $statuses
+            ],
 			'post_thumbnail'	=> [
 				'type'	=> 'upload',
 				'subtype'	=> 'image',
@@ -127,12 +133,6 @@
 				'label'	=> 'TLDR',
 				'maxLength'	=> 158,
 			],
-			'post_status'	=> [
-				'type'		=> 'radio',
-				'label'		=> 'Status',
-				'default'	=> 'draft',
-				'options'	=> $statuses
-			]
 		];
 
 		foreach ($fields as $name => $config) {
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index 6b5cf2c..8b4b57f 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -724,9 +724,10 @@
 	public function getSections():array
 	{
 		$allSections = array_map(function($section) {
-			return $section->getConfig;
+			return $section->getConfig();
 		}, $this->sections);
 
+
 		if (!empty($this->sectionOrder)) {
 			$allSections['order'] = $this->sectionOrder;
 		}
@@ -759,13 +760,11 @@
 			foreach ($sections as $s) {
 				$section = new Section($s, $this);
 				$section->setTitle(ucwords(implode(' ', explode('-', $s))));
-				$sectionFields = array_map(function ($f) {
-					return $f['name'];
-				}, array_filter($fields, function ($f) use ($s) {
+				$sectionFields = array_filter($fields, function ($f) use ($s) {
 					$tmp = array_key_exists('section', $f) && !is_null($f['section']) ? $f['section'] : 'main';
 					return $s === $tmp;
-				}));
-				$section->setFields($sectionFields);
+				});
+				$section->setFields(array_keys($sectionFields));
 				$this->sections[$s] = $section;
 			}
 		}
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index 32d7866..9f38917 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/inc/rest/routes/FavouritesRoutes.php
@@ -16,9 +16,6 @@
 	exit;
 }
 
-/**
- * TODO: Extract business logic into a Favourites.php manager class
- */
 class FavouritesRoutes extends Rest
 {
 	protected array $valid_types;
@@ -73,7 +70,6 @@
 			->post([$this, 'handleFavourite'])
 			->args([
 				'user' => 'integer|required',
-				'id' => 'string|required',
 				'action' => 'string|required|enum:add,remove,toggle,batch,note',
 				'type' => 'string',
 				'target_id' => 'integer',
@@ -130,149 +126,50 @@
 		if ($cache_check) {
 			return $cache_check;
 		}
-
-		if (count($args) === 1 || ($request->get_param('include_all') === true)) {
-			$result = $this->getAllFavourites($user_id);
-		} else {
-			$result = $this->cache->remember($key, function() use ($args) {
-				return $this->getFilteredFavourites($args);
-			});
-		}
+		$result = JVB()->favourites()->getFavourites($args);
+		$result['items'] = $this->formatItems($result['items']);
 
 		return $this->addCacheHeaders(Response::success($result));
 	}
 
-	/**
-	 * Get filtered favourites using CustomTable fluent interface
-	 */
-	protected function getFilteredFavourites(array $args): array
-	{
-		try {
-			// Build base query
-			$query = $this->favourites->where(['user_id' => $args['user']]);
-
-			// Add type filter if specified
-			if (!empty($args['content']) && $args['content'] !== 'all') {
-				$query = $this->favourites->where([
-					'user_id' => $args['user'],
-					'type' => BASE . $args['content']
-				]);
-			}
-
-			// Apply ordering and pagination
-			$orderby = in_array($args['orderby'] ?? 'date_added', ['date_added', 'type'])
-				? $args['orderby']
-				: 'date_added';
-			$order = in_array(strtoupper($args['order'] ?? 'DESC'), ['ASC', 'DESC'])
-				? strtoupper($args['order'])
-				: 'DESC';
-
-			$favourites = $query
-				->orderBy($orderby, $order)
-				->limit(100, ($args['page'] - 1) * 100)
-				->getResults();
-
-			// Get total count
-			$count_query = $this->favourites->where(['user_id' => $args['user']]);
-			if (!empty($args['content']) && $args['content'] !== 'all') {
-				$count_query->where(['type' => BASE . $args['content']]);
-			}
-			$total_items = $count_query->countResults();
-
-			return [
-				'items' => $this->formatItems($favourites),
-				'has_more' => ($args['page'] * 100) < $total_items,
-				'total' => $total_items,
-				'success' => true,
-			];
-
-		} catch (Exception $e) {
-			$this->logError('getFilteredFavourites', [
-				'error' => $e->getMessage(),
-				'args' => $args
-			]);
-
-			return [
-				'success' => false,
-				'items' => [],
-				'total' => 0,
-				'has_more' => false
-			];
-		}
-	}
-
-	/**
-	 * Get all user's favourites organized by content type
-	 */
-	protected function getAllFavourites(int $user_id): array
-	{
-		return $this->cache->remember($user_id, function() use ($user_id) {
-			try {
-				$favourites = $this->favourites
-					->where(['user_id' => $user_id])
-					->getResults();
-
-				$by_type = [];
-				foreach ($favourites as $fav) {
-					$type = str_replace(BASE, '', $fav->type);
-					if (!isset($by_type[$type])) {
-						$by_type[$type] = [];
-					}
-					$by_type[$type][] = (int)$fav->target_id;
-				}
-
-				return [
-					'success' => true,
-					'items' => $by_type,
-					'has_more' => false,
-				];
-
-			} catch (Exception $e) {
-				$this->logError('getAllFavourites', [
-					'error' => $e->getMessage(),
-					'user_id' => $user_id
-				]);
-
-				return [
-					'success' => false,
-					'items' => [],
-				];
-			}
-		});
-	}
 
 	/**
 	 * Handle favourite operations
 	 */
 	public function handleFavourite(WP_REST_Request $request): WP_REST_Response
 	{
-		$user_id = absint($request->get_param('user'));
-		$operation_id = sanitize_text_field($request->get_param('id'));
-		$action = sanitize_text_field($request->get_param('action'));
+		$params = $request->get_params();
+		$user_id = absint($params['user']??0);
 
 		if (!$this->userCheck($user_id)) {
 			return $this->unauthorized();
 		}
+		$action = strtolower(sanitize_text_field($params['action']));
+		$action = in_array($action, ['add', 'remove']) ? $action : false;
+		if (!$action) {
+			return $this->error('Invalid favourite action');
+		}
+		$target_id = absint($params['target_id']??0);
+		if ($target_id === 0) {
+			return $this->error('Invalid target id');
+		}
 
-		$data = [
-			'action' => $action,
-			'type' => sanitize_text_field($request->get_param('type') ?? ''),
-			'target_id' => absint($request->get_param('target_id') ?? 0),
-			'items' => $request->get_param('items') ?? [],
-			'notes' => sanitize_textarea_field($request->get_param('notes') ?? ''),
-		];
+		$type = sanitize_text_field($params['type']??'');
+		if (empty($type)) {
+			return $this->error('No type provided');
+		}
 
-		JVB()->queue()->queueOperation(
-			'favourite_' . $action,
+		$result = JVB()->favourites()->toggleFavourite(
+			$action === 'add',
 			$user_id,
-			$data,
-			[
-				'operation_id' => $operation_id,
-				'priority' => 'high',
-			]
+			$target_id,
+			$type
 		);
 
-		return $this->queued($operation_id);
+		if ($result) {
+			return $this->success();
+		}
+		return $this->error('Something went wrong');
 	}
 
 	/**
@@ -280,18 +177,21 @@
 	 */
 	public function getLists(WP_REST_Request $request): WP_REST_Response
 	{
-		$user_id = absint($request->get_param('user'));
+		$params = $request->get_params();
+		$user_id = absint($params['user']);
 
 		if (!$this->userCheck($user_id)) {
 			return $this->unauthorized();
 		}
 
-		$params = ['user' => $user_id];
-		if ($request->get_param('id')) {
-			$params['list'] = sanitize_text_field($request->get_param('id'));
+		$args = $this->buildParams($request);
+		$args['per_page'] = 20;
+		$listId = $request->get_param('id');
+		if (!empty($listId)) {
+			$args['where']['id'] = sanitize_text_field($listId);
 		}
 
-		$key = $this->listsCache->generateKey($params);
+		$key = $this->listsCache->generateKey($args);
 
 		// Check cache headers
 		$cache_check = $this->checkHeaders($request, $key);
@@ -299,94 +199,18 @@
 			return $cache_check;
 		}
 
-		$list_id = $request->get_param('id');
-		$response = $list_id
-			? $this->getListDetails($list_id, $user_id)
-			: $this->getAvailableLists($user_id);
+		$includeShares = !empty($request->get_param('include_shares'));
+
+		$response = !empty($listId)
+			? JVB()->favourites()->getListDetails($listId, $user_id)
+			: JVB()->favourites()->getAvailableLists($args, $includeShares);
 
 		return $this->addCacheHeaders(Response::success($response));
 	}
 
-	/**
-	 * Get lists available to a user using CustomTable
-	 */
-	protected function getAvailableLists(int $user_id, bool $include_shared = true): array
-	{
-		if (!$this->checkUser($user_id)) {
-			return [];
-		}
-
-		$cache = $include_shared ? $this->sharedListsCache : $this->listsCache;
-
-		return $cache->remember($user_id, function() use ($user_id, $include_shared) {
-			try {
-				// Get owned lists
-				$owned = $this->lists
-					->where(['user_id' => $user_id])
-					->orderBy('created_at', 'DESC')
-					->getResults(ARRAY_A);
-
-				// Add item counts
-				foreach ($owned as &$list) {
-					$list['item_count'] = $this->listItems
-						->where(['list_id' => $list['id']])
-						->countResults();
-					$list['is_owner'] = true;
-					$list['is_shared'] = false;
-				}
-
-				if (!$include_shared) {
-					return [
-						'success' => true,
-						'lists' => $owned
-					];
-				}
-
-				// Get shared lists
-				$shares = $this->listShares
-					->where(['user_id' => $user_id, 'status' => 'accepted'])
-					->getResults();
-
-				$shared_lists = [];
-				foreach ($shares as $share) {
-					$list = $this->lists
-						->where(['id' => $share->list_id])
-						->first(ARRAY_A);
-
-					if ($list) {
-						$owner = get_userdata($list['user_id']);
-						$list['owner_name'] = $owner ? $owner->display_name : 'Unknown';
-						$list['item_count'] = $this->listItems
-							->where(['list_id' => $list['id']])
-							->countResults();
-						$list['permission_type'] = $share->permission_type;
-						$list['is_owner'] = false;
-						$list['is_shared'] = true;
-
-						$shared_lists[] = $list;
-					}
-				}
-
-				return [
-					'success' => true,
-					'lists' => [
-						'owned' => $owned,
-						'shared' => $shared_lists
-					]
-				];
-
-			} catch (Exception $e) {
-				$this->logError('getAvailableLists', [
-					'error' => $e->getMessage(),
-					'user_id' => $user_id
-				]);
-
-				return [];
-			}
-		});
-	}
 
 	/**
+	 * TODO: Done until here
 	 * Get favourite counts by type
 	 */
 	public function getFavouriteCounts(WP_REST_Request $request): WP_REST_Response
@@ -840,18 +664,22 @@
 	protected function buildParams(WP_REST_Request $request): array
 	{
 		$data = $request->get_params();
-		$args = ['user' => absint($data['user'])];
 
-		if (!array_key_exists('page', $data)) {
-			return $args;
+		$where = ['user_id' => absint($data['user'])];
+		if (!empty($data['content']) && $data['content'] !== 'all') {
+			$where['type'] = BASE . $data['content'];
 		}
 
-		$args = array_merge($args, [
-			'page' => max(1, absint($data['page'] ?? 1)),
-			'content' => Registrar::getInstance($data['content']) ? $data['content'] : 'all',
-		]);
+		$page    = max(1, absint($data['page'] ?? 1));
+		$perPage = 250;
 
-		return $this->applyOrderFilters($args, $data);
+		return [
+			'where'		=> $where,
+			'orderby' 	=> sanitize_text_field($data['orderby'] ?? 'created_at'),
+			'order'		=> sanitize_text_field($data['order'] ?? 'DESC'),
+			'per_page'	=> $perPage,
+			'page'		=> $page
+		];
 	}
 
 
@@ -1034,40 +862,35 @@
 	 */
 	protected function getListDetails(int $list_id, int $user_id): array
 	{
+		// Check access - either owner or has share
+		$is_owner = JVB()->favourites()->userOwnsList($list_id, $user_id);
+		$is_shared = JVB()->favourites()->userCanViewList($list_id, $user_id);
+
+
+		if (!$is_owner && !$is_shared) {
+			return [
+				'success'	=> false,
+				'message'	=> 'You do not have access to this list.'
+			];
+		}
+
+		$list = JVB()->favourites()->getListDetails($list_id, $user_id);
+
+		if (empty($list)) {
+			return [
+				'success' => false,
+				'message' => 'List not found'
+			];
+		}
+
+
+
 		$key = "list_{$list_id}_user_{$user_id}";
 
 		return $this->listsCache->remember($key, function () use ($list_id, $user_id) {
 			try {
-				// Check access - either owner or has share
-				$is_owner = $this->lists->where([
-					'id' => $list_id,
-					'user_id' => $user_id
-				])->existsInQuery();
 
-				$share = null;
-				if (!$is_owner) {
-					$share = $this->listShares->where([
-						'list_id' => $list_id,
-						'user_id' => $user_id,
-						'status' => 'accepted'
-					])->first();
-				}
 
-				if (!$is_owner && !$share) {
-					return [
-						'success' => false,
-						'message' => 'You do not have access to this list'
-					];
-				}
-
-				// Get list details
-				$list = $this->lists->where(['id' => $list_id])->first(ARRAY_A);
-				if (!$list) {
-					return [
-						'success' => false,
-						'message' => 'List not found'
-					];
-				}
 
 				// Get list items
 				$items = $this->listItems
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index d1d4834..2e35ae0 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -1579,7 +1579,7 @@
 			<input type="hidden" name="content" value="<?=$this->dataType?>" />
 			<div class="fields">
 				<?php
-				if (!empty($this->statuses)) {
+				if (empty($this->sections) && !empty($this->statuses)) {
 					echo Form::render('post_status', '', $this->getStatusFieldConfig('edit-'));
 				}
 
@@ -1593,8 +1593,9 @@
 								'icon'	=> $config['icon']
 							];
 						}
+
 						$tabs[$slug] = array_merge([
-							'title'	=> $config['label'],
+							'title'	=> $config['title'],
 							'content' => '',
 							'description' => $config['description']??'',
 						], $section);
@@ -1615,7 +1616,7 @@
 					foreach ($first as $f) {
 						if (array_key_exists($f, $fields)) {
 							if ($tabs) {
-								$tabs['basic']['content'] .= Form::render($f, '', $fields[$f]);
+								$tabs['main']['content'] .= Form::render($f, '', $fields[$f]);
 							} else {
 								echo Form::render($f, '', $fields[$f]);
 							}
@@ -1660,12 +1661,13 @@
 
 					$fields = $this->nonTimelineFields;
 				}
+
 				foreach ($fields as $n => $config) {
 					if (in_array($config['type'], ['taxonomy', 'selector'])) {
 						$config = array_merge($config, $this->taxConfig($config['taxonomy'], $config['label']));
 					}
 					if ($tabs) {
-						$section = (array_key_exists('section', $config)) ? $config['section'] : 'basic';
+						$section = (array_key_exists('section', $config)) && !empty($config['section']) ? $config['section'] : 'main';
 						$tabs[$section]['content'] .= Form::render($n, '', $config);
 					} else {
 						echo Form::render($n, '', $config);

--
Gitblit v1.10.0