From 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Nov 2025 07:42:23 +0000
Subject: [PATCH] =Feed block mostly good! Referrals look good to go. Ready for Madi and Heidi to approve

---
 inc/managers/CRUDManager.php |  349 ++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 300 insertions(+), 49 deletions(-)

diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index 9589fdc..e05a0e5 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -2,7 +2,9 @@
 namespace JVBase\managers;
 
 use JVBase\managers\UserTermsManager;
+use JVBase\meta\MetaForm;
 use JVBase\meta\MetaManager;
+use JVBase\utility\Features;
 use WP_User;
 
 if (!defined('ABSPATH')) {
@@ -19,12 +21,20 @@
 	protected array $filters;
 	protected array $bulkActions;
 	protected MetaManager $meta;
+	protected MetaForm $form;
 	protected array $taxonomies;
 	protected array $statuses;
 	protected array $fields;
 	protected array $sections;
 	protected array $stuck;
 
+
+	//For Timeline-specific posts
+	protected bool $isTimeline = false;
+	protected array $nonTimelineFields = [];
+	protected array $timelineSharedFields = [];
+	protected array $timelineUniqueFields = [];
+
 	protected bool $userCanPublish = false;
 
 	public function __construct(string $content)
@@ -40,6 +50,7 @@
 		$this->plural = $this->config['plural'];
 		$this->content = $content;
 		$this->fields = jvbGetFields($this->content, 'post');
+		$this->maybeSetupTimeline();
 		$this->sections = jvbGetSections($this->content, 'post');
 		$this->stuck = [
 			'post_title',
@@ -47,6 +58,11 @@
 		];
 
 		$this->init();
+
+		if ($this->isTimeline) {
+			$this->stuck[] = 'post_thumbnail';
+		}
+		add_filter('jvbAdditionalActions', [$this, 'createItem']);
 	}
 
 	protected function init():void
@@ -56,13 +72,42 @@
 		$this->initTaxonomies();
 		$this->initFilters();
 		$this->meta = new MetaManager(null, 'post', $this->content);
-
+		$this->form = new MetaForm();
 		$plural = strtolower($this->config['plural']??$this->content.'s');
 		$this->userCanPublish = (jvbUserIsVerified()) ?
 					user_can($this->user_id, "publish_{$plural}") : false;
 
 	}
 
+	protected function maybeSetupTimeline():void {
+		$this->isTimeline = Features::forContent($this->content)->has('is_timeline');
+
+		if (!$this->isTimeline) {
+			return;
+		}
+		$this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
+			if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
+				return true;
+			}
+			return false;
+		}));
+		array_unshift($this->timelineSharedFields, 'post_thumbnail');
+		array_unshift($this->timelineSharedFields, 'post_title');
+		array_unshift($this->timelineSharedFields, 'post_status');
+
+		$this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
+			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
+				return true;
+			}
+			return false;
+		}));
+
+		$all = array_merge($this->timelineUniqueFields, $this->timelineSharedFields);
+		$this->nonTimelineFields = array_filter($this->fields, function ($field) use ($all) {
+			return !in_array($field, $all);
+		}, ARRAY_FILTER_USE_KEY);
+	}
+
 	protected function initTaxonomies():void
 	{
 		$this->taxonomies = array_filter(JVB_TAXONOMY, function ($config) {
@@ -80,41 +125,41 @@
 				],
 				'future'=> [
 					'label'	=> 'Upcoming',
-					'icon'	=> 'future',
+					'icon'	=> 'clock-clockwise',
 				],
 				'past'	=> [
 					'label'	=> 'Past',
-					'icon'	=> 'past',
+					'icon'	=> 'clock-counter-clockwise',
 				],
 				'repeat'=> [
 					'label'	=> 'Recurring',
 					'icon'	=> 'repeat',
 				],
 				'draft'	=> [
-					'icon'	=> 'hide',
+					'icon'	=> 'eye-closed',
 					'label'	=> 'Hidden',
 				],
 				'trash'	=> [
 					'label'	=> 'Scrapped',
-					'icon'	=> 'delete',
+					'icon'	=> 'trash',
 				],
 			] :
 			[
 				'all'	=> [
-					'icon'  => 'all',
+					'icon'  => 'infinity',
 					'label' => 'Everything',
 				],
 				'publish'=> [
-					'icon'	=> 'publish',
+					'icon'	=> 'eye',
 					'label'	=> 'Live',
 				],
 				'draft'	=> [
-					'icon'	=> 'hide',
+					'icon'	=> 'eye-closed',
 					'label'	=> 'Hidden',
 				],
 				'trash'	=> [
 					'label'	=> 'Scrapped',
-					'icon'	=> 'delete',
+					'icon'	=> 'trash',
 				],
 			];
 	}
@@ -143,7 +188,7 @@
 		foreach ($this->taxonomies as $taxonomy=> $config) {
 			$this->filters['taxonomy'][$taxonomy] = [
 				'label'	=> $config['singular'],
-				'icon'	=> $taxonomy
+				'icon'	=> $config['icon']??'folder'
 			];
 		}
 	}
@@ -152,7 +197,7 @@
 	{
 		ob_start();
 		?>
-		<div class="dashboard-page <?= esc_attr($this->content) ?>">
+		<div class="dashboard-page <?= esc_attr($this->content) ?>"<?=($this->isTimeline) ? ' data-timeline' : ''?>>
 			<?php
 			$this->renderHeader();
 			$this->renderContent();
@@ -181,7 +226,8 @@
 	protected function renderHeaderActions():void
 	{
 		$uploadConfig = [
-			'type'			=> 'image',
+			'type'			=> 'upload',
+			'subtype'		=> 'image',
 			'mode'			=> (jvbCheck('single_image', $this->config)) ? 'direct' : 'selection',
 			'create_new'	=> true,
 			'label'			=> (array_key_exists('image_title', $this->config)) ? $this->config['image_title'] : 'Upload More '.$this->config['plural'],
@@ -189,14 +235,15 @@
 			'singular'		=> $this->singular,
 			'plural'		=> $this->plural,
 			'multiple'		=> true,
+			'destination'	=> 'post'
 		];
-		if (!jvbCheck('single_image', $this->config)) {
-			$uploadConfig['imageType'] = 'groupable';
+		if (!array_key_exists('single_image', $this->config) || $this->config['single_image'] === false) {
+			$uploadConfig['destination'] = 'post_group';
 		}
-
+		$uploadConfig['destination'] = 'post_group';
 		if (!jvbCheck('single_image', $this->config)) {
-			$uploadConfig['group_title'] = 'Create '.$this->config['plural'];
-			$uploadConfig['group_description'] = '<p>Drag images into groups. Each group becomes its own '.$this->singular.'.</p>
+			$uploadConfig['label'] = 'Create '.$this->config['plural'];
+			$uploadConfig['upload_text'] = '<p>Drag images into groups. Each group becomes its own '.$this->singular.'.</p>
 						<p>You can also select multiple images and click the "Add to Group" button.</p>
 						<p>If a '.$this->singular.' has multiple images, you can select the '.jvbIcon('star').' to set an image as the main one.</p>
 						<p>Images left ungrouped will become individual '.$this->plural.'</p>
@@ -205,7 +252,6 @@
 			$uploadConfig['description'] = 'Each image will become its own '.$this->singular.'.';
 		}
 		?>
-		<button type="button" class="create-item row" title="Create New <?= $this->singular?>"><?=jvbIcon('add') ?><span class="screen-reader-text">Create New <?= $this->singular?></span></button>
 		<details open class="uploader">
 			<summary class="row btw"><?= $this->config['upload_title'] ?? 'Bulk Upload '.$this->plural?></summary>
 			<?php
@@ -243,9 +289,9 @@
 		ob_start();
 		?>
 		<div class="empty-state">
-			<h3><?=jvbIcon($this->content)?>Nothing here<?=jvbIcon($this->content)?></h3>
+			<h3><?=jvbIcon($this->config['icon'])?>Nothing here<?=jvbIcon($this->config['icon'])?></h3>
 			<p>It doesn't look like you have any <?=$this->config['plural'] ?> yet.</p>
-			<p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('add')?>" button to add one at a time.</small></p>
+			<p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('plus-square')?>" button to add one at a time.</small></p>
 		</div>
 		<?php
 		return ob_get_clean();
@@ -254,7 +300,7 @@
 	protected function renderFilters():void
 	{
 		?>
-		<div class="all-filters col start">
+		<div class="all-filters col start" data-ignore>
 			<div class="search row start nowrap">
 				<span class="label">Search:</span>
 				<?= jvbSearch() ?>
@@ -273,7 +319,7 @@
 					$this->renderDateFilters();
 				?>
 				<button type="button" class="clear-filters row" hidden>
-					<?= jvbIcon('close', ['title'    => 'Clear']); ?>
+					<?= jvbIcon('x', ['title'    => 'Clear']); ?>
 					Clear All Filters
 				</button>
 			</div>
@@ -294,8 +340,8 @@
 						'alphabetical' => 'Order alphabetically'
 					],
 					'order' => [
-						'asc' => 'In ascending order (Z-A, oldest to newest)',
-						'desc' => 'In descending order (A-Z, newest to oldest)'
+						'sort-ascending' => 'In ascending order (Z-A, oldest to newest)',
+						'sort-descending' => 'In descending order (A-Z, newest to oldest)'
 					]
 				];
 
@@ -306,10 +352,11 @@
 					<?php
 					$i = 0;
 					foreach ($option as $opt => $label) {
+						$icon = $opt === 'date' ? 'calendar' : $opt;
 						?>
 						<input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$opt?>"<?=$i===0 ? ' checked':''?>>
 
-						<label for="<?=$opt?>" title="<?=$label?>"><?=jvbIcon($opt)?></label>
+						<label for="<?=$opt?>" title="<?=$label?>"><?=jvbIcon($icon)?></label>
 						<?php
 						$i++;
 					}
@@ -355,8 +402,8 @@
 
 			<?php
 			$views = [
-				'grid' => ['icon' => 'grid', 'label' => 'Grid View'],
-				'list' => ['icon' => 'list', 'label' => 'List View'],
+				'grid' => ['icon' => 'squares-four', 'label' => 'Grid View'],
+				'list' => ['icon' => 'rows', 'label' => 'List View'],
 				'table' => ['icon' => 'table', 'label' => 'Table View']
 			];
 
@@ -583,7 +630,7 @@
 
 	protected function renderModals():void
 	{
-		$this->renderCreateModal();
+//		$this->renderCreateModal();
 		$this->renderEditModal();
 		$this->renderBulkEditModal();
 	}
@@ -600,7 +647,8 @@
 	{
 		ob_start();
 		?>
-		<form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>">
+		<form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>" data-autosave<?= ($this->isTimeline) ? ' data-timeline' : ''?>>
+			<?= jvbFormStatus() ?>
 			<input type="hidden" name="form-id" value="<?=uniqid('new-')?>" />
 			<input type="hidden" name="content" value="<?=$this->content?>" />
 			<div class="fields">
@@ -630,20 +678,50 @@
 				} else {
 					$tabs = false;
 				}
+
+
 				$fields = $this->fields;
+				if (!$this->isTimeline) {
+					$first = ['post_thumbnail', 'post_title', 'price'];
 
-				$first = ['post_thumbnail', 'post_title', 'price'];
-				foreach ($first as $f) {
-					if (array_key_exists($f, $fields)) {
-						if ($tabs) {
-							$tabs['basic']['content'] .= $this->meta->render('form', $f, $fields[$f], false, true);
-						} else {
-							$this->meta->render('form', $f, $fields[$f]);
+					foreach ($first as $f) {
+						if (array_key_exists($f, $fields)) {
+							if ($tabs) {
+								$tabs['basic']['content'] .= $this->meta->render('form', $f, $fields[$f], false, true);
+							} else {
+								$this->meta->render('form', $f, $fields[$f]);
+							}
+
+							unset($fields[$f]);
 						}
-
-						unset($fields[$f]);
 					}
 				}
+
+				if ($this->isTimeline) {
+					$temp = array_filter($fields, function ($field) {
+						return in_array($field, $this->timelineUniqueFields);
+					}, ARRAY_FILTER_USE_KEY);
+
+					$config = [
+						'type'		=> 'gallery',
+						'subtype'	=> 'timeline',
+						'data'		=> 'timeline',
+						'label'		=> 'Progression',
+						'fields'	=> $temp
+					];
+					$content = '';
+					foreach ($fields as $slug=> $field) {
+						if (in_array($slug, $this->timelineSharedFields)) {
+							$content .= $this->form->render($slug, null, $field, false, true);
+						}
+					}
+
+
+					$content .= $this->meta->render('form', 'timeline', $config, false,true);
+
+					$tabs['progression']['content'] = $content;
+					$fields = $this->nonTimelineFields;
+				}
 				foreach ($fields as $n => $config) {
 					if ($tabs) {
 						$section = (array_key_exists('section', $config)) ? $config['section'] : 'basic';
@@ -651,10 +729,8 @@
 					} else {
 						$this->meta->render('form', $n, $config);
 					}
-
 				}
 
-
 				if ($tabs) {
 					jvbRenderTabs($tabs);
 				}
@@ -665,6 +741,28 @@
 		return ob_get_clean();
 	}
 
+
+	protected function renderRowFields():void
+	{
+		$fields = $this->fields;
+
+		// Render priority fields first
+		$first = ['post_thumbnail', 'post_title', 'price'];
+		foreach ($first as $f) {
+			if (array_key_exists($f, $fields)) {
+				$this->meta->render('form', $f, $fields[$f]);
+				unset($fields[$f]);
+			}
+		}
+
+		// Render remaining fields
+		foreach ($fields as $name => $config) {
+			if (!array_key_exists('hidden', $config) || !$config['hidden']) {
+				$this->meta->render('form', $name, $config);
+			}
+		}
+	}
+
 	protected function getApplicableStatuses(string $prefix) {
 		foreach ($this->statuses as $status => $config) {
 			if ($status === 'all') {
@@ -674,7 +772,7 @@
 				if ($status === 'future') {
 					$status = 'publish';
 					$config = [
-						'icon'	=> 'publish',
+						'icon'	=> 'eye',
 						'label'	=> 'Live',
 					];
 				} else {
@@ -711,6 +809,7 @@
 		ob_start();
 		?>
 		<form class="bulk-edit-form" data-save="content" data-form-id="bulk-edit-<?=$this->content?>">
+			<?= jvbFormStatus() ?>
 			<div class="selected"></div>
 			<p class="description">You can unselect items by clicking the image here.</p>
 			<p class="hint"><strong>IMPORTANT: </strong> Whatever changes you make here will be applied to all selected <?=$this->plural?>.</p>
@@ -774,6 +873,15 @@
 		$this->renderGridView();
 		$this->renderTableView();
 		$this->renderTableRow();
+		if ($this->isTimeline) {
+			$temp = array_filter($this->fields, function ($field) {
+				return in_array($field, $this->timelineUniqueFields);
+			}, ARRAY_FILTER_USE_KEY);
+			$form = new MetaForm();
+			echo '<template class="timelineItem">';
+			$form->renderImagePreview(null,['fields' => $temp]);
+			echo '</template>';
+		}
 		echo jvbGetEmptyStateTemplate();
 		echo jvbGetGalleryPreviewTemplate();
 
@@ -809,11 +917,11 @@
 		?>
 		<div class="item-actions">
 			<button type="button" class="action" data-action="edit" title="Edit <?= $this->singular ?>">
-				<?=jvbIcon('edit')?>
+				<?=jvbIcon('pencil-simple')?>
 				<span class="screen-reader-text">Edit <?= $this->singular ?></span>
 			</button>
 			<button type="button" class="action" data-action="trash" title="Scrap <?= $this->singular ?>">
-				<?=jvbIcon('delete')?>
+				<?=jvbIcon('trash')?>
 				<span class="screen-reader-text">Scrap <?= $this->singular ?></span>
 			</button>
 <!--			<button type="button" class="action" data-action="toggle-status">-->
@@ -878,13 +986,17 @@
 
 	protected function renderTableView():void
 	{
+		if ($this->isTimeline) {
+			$this->renderTimelineTableView();
+			return;
+		}
 		?>
 		<template class="contentTable">
 			<form class="table"
 				  data-save="content"
 				  data-content="<?= esc_attr($this->content) ?>"
 				  data-form-id="content-table-<?= esc_attr($this->content) ?>">
-
+				<?= jvbFormStatus() ?>
 				<?= $this->renderTableActions() ?>
 
 				<table>
@@ -906,13 +1018,17 @@
 	 * Render table row template
 	 */
 	protected function renderTableRow(): void {
+		if ($this->isTimeline) {
+			$this->renderTimelineTableGroup();
+			return;
+		}
 		?>
 		<template class="tableView">
 			<tr class="item">
 				<td class="select">
 					<?= $this->renderItemSelect() ?>
 				</td>
-				<td class="status">
+				<td class="status" data-field="post_status">
 					<?= $this->renderStatusRadios() ?>
 				</td>
 				<?php
@@ -938,6 +1054,102 @@
 		</template>
 		<?php
 	}
+
+	protected function renderTimelineTableView():void
+	{
+		?>
+		<template class="contentTable">
+			<form class="table"
+				  data-save="content"
+				  data-content="<?= esc_attr($this->content) ?>"
+				  data-form-id="content-table-<?= esc_attr($this->content) ?>">
+				<?= jvbFormStatus() ?>
+				<?= $this->renderTableActions() ?>
+
+				<table>
+					<thead>
+					<?= $this->renderTimelineTableHeader() ?>
+					</thead>
+					<!-- Rows are inserted as tbody groups -->
+					<tfoot>
+					<?= $this->renderTimelineTableHeader() ?>
+					</tfoot>
+				</table>
+			</form>
+		</template>
+		<?php
+	}
+
+	protected function renderTimelineTableGroup():void
+	{
+		$makeDetails = [
+			'group',
+			'repeater',
+			'checkbox',
+			'radio'
+		];
+		?>
+		<template class="tableView">
+			<tbody class="item">
+				<tr class="shared">
+					<td class="select">
+						<?= $this->renderItemSelect() ?>
+					</td>
+					<td class="show-post_status field" data-field="post_status">
+						<?= $this->renderStatusRadios() ?>
+					</td>
+					<?php
+					foreach ($this->fields as $name => $config) {
+						if(array_key_exists('hidden', $config) || $name === 'post_status') {
+							continue;
+						}
+						if (!in_array($name, $this->timelineSharedFields)) {
+							echo '<td></td>';
+							continue;
+						}
+							$makeThisDetailed = (in_array($config['type'], $makeDetails));
+						?>
+						<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+							<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+							<?php $this->meta->render('form', $name, $config); ?>
+							<?= $makeThisDetailed ? '</details>' : '' ?>
+						</td>
+						<?php
+					}
+
+					?>
+				</tr>
+				<tr class="timeline-point">
+					<td class="select">
+						<button class="drag-handle" title="Drag to reorder" aria-label="Drag to reorder this timeline point"><?= jvbIcon('dots-six') ?></button>
+					</td>
+					<td class="show-post_status field" data-field="post_status">
+						<?= $this->renderStatusRadios() ?>
+					</td>
+					<?php
+					foreach ($this->fields as $name => $config) {
+						if(array_key_exists('hidden', $config) || $name === 'post_status') {
+							continue;
+						}
+						if (!in_array($name, $this->timelineUniqueFields)) {
+							echo '<td></td>';
+							continue;
+						}
+						$makeThisDetailed = (in_array($config['type'], $makeDetails));
+						?>
+						<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+							<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+							<?php $this->meta->render('form', $name, $config); ?>
+							<?= $makeThisDetailed ? '</details>' : '' ?>
+						</td>
+						<?php
+					}
+					?>
+				</tr>
+			</tbody>
+		</template>
+		<?php
+	}
 	/**
 	 * Render status radio buttons
 	 */
@@ -952,7 +1164,7 @@
 				if ($status === 'future') {
 					$status = 'publish';
 					$config = [
-						'icon' => 'publish',
+						'icon' => 'eye',
 						'label' => 'Live'
 					];
 				} elseif ($status === 'past') {
@@ -999,6 +1211,32 @@
 		return ob_get_clean();
 	}
 
+	protected function renderTimelineTableHeader(): string {
+		ob_start();
+
+		?>
+		<tr>
+			<th scope="col" class="select-header">
+				<input type="checkbox" id="select-all" name="select-all">
+				<label for="select-all">All</label>
+			</th>
+			<th scope="col" class="show-post_status">
+				Status
+			</th>
+			<?php foreach ($this->fields as $name => $config):
+				if (array_key_exists('hidden', $config) || $name === 'post_status'){
+					continue;
+				}
+				?>
+				<th scope="col" class="show-<?= esc_attr($name) ?>"<?= (in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+					<?= esc_html($config['label']) ?>
+				</th>
+			<?php endforeach; ?>
+		</tr>
+		<?php
+		return ob_get_clean();
+	}
+
 	/**
 	 * Render table action controls
 	 */
@@ -1010,16 +1248,29 @@
 				'vertical',
 				'TAB NAV:',
 				'',
-				jvbIcon('down'),
-				jvbIcon('right')
+				jvbIcon('caret-double-down'),
+				jvbIcon('caret-double-right')
 			) ?>
 
 			<button type="button" class="add-row" title="Add new row">
-				<?= jvbIcon('add') ?>
+				<?= jvbIcon('plus-square') ?>
 				<span>Add Row</span>
 			</button>
 		</div>
 		<?php
 		return ob_get_clean();
 	}
+
+	public function createItem(array $actions):array
+	{
+		ob_start();
+		$this->renderCreateModal();
+		$content = ob_get_clean();
+		$create = [
+			'button'	=> '<button type="button" class="create-item row" title="Create New '.$this->singular.'">'.jvbIcon('plus-square').'<span class="screen-reader-text">Create New '.$this->singular.'</span></button>',
+			'content'	=> $content,
+		];
+		$actions[] = $create;
+		return $actions;
+	}
 }

--
Gitblit v1.10.0