From d7dbe7fee362d587dfc334135d9581b6216a4295 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 23 Nov 2025 04:13:56 +0000
Subject: [PATCH] =Timeline block, and feed block updated. DataStore.js refactored to not block rendering

---
 inc/managers/IconsManager.php |  301 +++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 262 insertions(+), 39 deletions(-)

diff --git a/inc/managers/IconsManager.php b/inc/managers/IconsManager.php
index d3710d0..8278763 100644
--- a/inc/managers/IconsManager.php
+++ b/inc/managers/IconsManager.php
@@ -17,6 +17,7 @@
 	protected array $customIcons = [];
 	protected array $usedIcons = [];
 	protected array $map = [];
+	protected const MAX_VERSIONS = 5;
 
 	/**
 	 * Get singleton instance
@@ -31,6 +32,7 @@
 	private function __construct()
 	{
 		$this->cache = CacheManager::for('icons', WEEK_IN_SECONDS);
+
 		$this->style = (array_key_exists('icons', JVB_SITE) && in_array(JVB_SITE['icons'], $this->styles))
 			? JVB_SITE['icons']
 			: 'regular';
@@ -44,36 +46,35 @@
 		]);
 
 
-		$this->usedIcons = get_option(BASE.'used_icons', []);
+		$this->usedIcons = get_option(BASE.'usedIcons', []);
 		$this->includeIcons();
+		// Track custom icons for CSS generation
+		$this->trackCustomIcons();
 		// Register hooks only once
 		$this->registerHooks();
 	}
 
 	/**
-	 * Include icons via filter (for JS usage, etc.)
+	 * Ensure custom icons are tracked for CSS generation
 	 */
-	protected function includeIcons():array
+	protected function trackCustomIcons(): void
 	{
-		$icons = get_option(BASE.'includeIcons');
-//		$icons = false;
-		if (!$icons) {
-			$icons = $this->addIncludeIcons();
-		}
-		$include = apply_filters('jvbIncludeIcons', []);
-		$add = array_filter($include, function($addIt) use ($icons) {
-			return !in_array($addIt, $icons);
-		});
-		error_log('Adding icons: '.print_r($add, true));
-		if (!empty($add)) {
-			$this->addIncludeIcons($add);
+		if (empty($this->customIcons)) {
+			return;
 		}
 
-		return $icons;
+		foreach ($this->customIcons as $name => $path) {
+			$this->trackIconUsage($name, $this->style);
+		}
 	}
 
-	protected function addIncludeIcons(array $add = []) {
+	/**
+	 * Include icons via filter (for JS usage, etc.)
+	 */
+	protected function includeIcons():void
+	{
 		$icons = get_option(BASE.'includeIcons');
+
 		if (!$icons) {
 			$icons = [
 				'check-circle',
@@ -120,6 +121,7 @@
 				'file-txt',
 				'file-xls',
 			];
+
 			$check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER];
 			foreach ($check as $constant) {
 				foreach ($constant as $key => $value) {
@@ -129,20 +131,77 @@
 				}
 			}
 			$icons = apply_filters('jvbIncludeIcons', $icons);
-
+			$icons = $this->maybePrefixIcons($icons);
 			update_option(BASE.'includeIcons', $icons);
-			update_option(BASE.'icons_needs_update', true);
 		}
-		$add = apply_filters('jvbIncludeIcons', $add);
-		$add = array_filter($add, function($addIt) use ($icons) {
-			return !in_array($addIt, $icons);
-		});
-		if (!empty($add)) {
-			$icons = array_merge($add, $icons);
-			update_option(BASE.'usedIcons', $icons);
-			update_option(BASE.'icons_needs_update', true);
+
+		// Ensure icons are in the correct format (handle legacy data)
+		if (!$this->isIconsArrayPrefixed($icons)) {
+			$icons = $this->maybePrefixIcons($icons);
+			update_option(BASE.'includeIcons', $icons);
 		}
-		return $icons;
+
+		$additional = apply_filters('jvbIncludeIcons', []);
+		if (!empty($additional)) {
+			$additional = $this->maybePrefixIcons($additional);
+			$merged = $this->mergeUsedIcons($icons, $additional);
+
+			if ($icons != $merged) {
+				update_option(BASE.'includeIcons', $merged);
+				$icons = $merged;
+			}
+		}
+
+		foreach ($icons as $style => $theIcons) {
+			foreach($theIcons as $icon) {
+				$this->trackIconUsage($icon, $style);
+			}
+		}
+	}
+
+	/**
+	 * Check if icons array is in the prefixed format [style => [icons]]
+	 */
+	protected function isIconsArrayPrefixed(array $icons): bool
+	{
+		if (empty($icons)) {
+			return true;
+		}
+
+		// Check if first key is a valid style name
+		$first_key = array_key_first($icons);
+		if (!in_array($first_key, $this->styles)) {
+			return false;
+		}
+
+		// Check if first value is an array
+		return is_array($icons[$first_key]);
+	}
+
+	protected function maybePrefixIcons(array $icons):array
+	{
+		$out = [];
+		foreach ($icons as $icon) {
+			if (is_array($icon) && array_key_exists('style', $icon)) {
+				if (!array_key_exists($icon['style'], $out)) {
+					$out[$icon['style']] = [];
+				}
+				if (!in_array($icon['name'], $out[$icon['style']])) {
+					$out[$icon['style']][] = $icon['name'];
+				}
+			} elseif(is_array($icon)) {
+				$icon = $icon['name'];
+			}
+			if (!is_array($icon)) {
+				if (!array_key_exists($this->style, $out)) {
+					$out[$this->style] = [];
+				}
+				if (!in_array($icon, $out[$this->style])){
+					$out[$this->style][] = $icon;
+				}
+			}
+		}
+		return $out;
 	}
 
 	protected function addMap():void
@@ -173,7 +232,8 @@
 	 */
 	protected function registerHooks(): void
 	{
-		add_action('init', [$this, 'checkCSS']);
+		add_action('init', [$this, 'includeIcons'], 1);
+		add_action('init', [$this, 'checkCSS'], 10);
 		add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']);
 		add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']);
 	}
@@ -192,7 +252,15 @@
 	{
 		error_log('[IconsManager]:regenerateCSS');
 		$css = $this->generateIconCSS();
-		$css_path = JVB_DIR.'/assets/css/icons.css';
+		$css_path = JVB_CHILD_DIR.'/assets/css/';
+		if (!file_exists($css_path)) {
+			wp_mkdir_p($css_path);
+		}
+		$css_path .= '/icons.css';
+
+
+		// Archive current version before overwriting
+		$this->archiveCurrentVersion($css);
 
 		if (file_put_contents($css_path, $css) !== false) {
 			CacheManager::updateTimestamp('icons');
@@ -228,24 +296,23 @@
 	 */
 	public function getIcon(string $name, array $options = []): string
 	{
-		if (!array_key_exists('style', $options)) {
-			$options['style'] = $this->style;
-		}
+		$style = array_key_exists('style', $options) ? $options['style'] :$this->style;
 		$name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name;
 
 		// Validate icon exists
-		if (!$this->iconExists($name, $options['style'])) {
+		if (!$this->iconExists($name, $style)) {
 			error_log('[IconsManager] Icon not found: ' . $name);
 			return '';
 		}
 
-		$style = $options['style'] ?? $this->style;
+
 
 		// Track icon usage
 		$this->trackIconUsage($name, $style);
 
+		$styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
 		// Build classes
-		$classes = ['icon', 'icon-' . $name];
+		$classes = ['icon', 'icon-' . $name.$styleClass];
 		if (!empty($options['class'])) {
 			$classes[] = $options['class'];
 		}
@@ -264,13 +331,24 @@
 	 */
 	protected function trackIconUsage(string $name, string $style): void
 	{
+		$needsUpdate = false;
+
 		if (!array_key_exists($style, $this->usedIcons)) {
 			$this->usedIcons[$style] = [];
+			$needsUpdate = true;
 		}
 
 		if (!in_array($name, $this->usedIcons[$style])) {
 			$this->usedIcons[$style][] = $name;
-			update_option(BASE.'used_icons', $this->usedIcons);
+			$needsUpdate = true;
+		}
+
+		if ($needsUpdate) {
+			// Merge with existing option to never lose icons
+			$existing = get_option(BASE.'usedIcons', []);
+			$merged = $this->mergeUsedIcons($existing, $this->usedIcons);
+			update_option(BASE.'usedIcons', $merged);
+
 			// Flag for regeneration on next init
 			update_option(BASE.'icons_needs_update', true);
 
@@ -357,7 +435,7 @@
 
 		wp_enqueue_style(
 			'jvb-icons',
-			JVB_URL.'assets/css/icons.css',
+			JVB_CHILD_URL.'assets/css/icons.css',
 			[],
 			$timestamp
 		);
@@ -369,10 +447,11 @@
 	protected function generateIconCSS(): string
 	{
 		$css = '';
+		$this->mergeUsedIcons();
+
 		foreach ($this->usedIcons as $style => $icons) {
 			$styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
 			foreach ($icons as $icon) {
-
 				$svg = $this->getEncodedSVG($icon, $style);
 				if ($svg !== '') {
 					$css .= ".icon-{$icon}{$styleClass}{";
@@ -384,6 +463,34 @@
 		return $this->minifyCss($css);
 	}
 
+	protected function mergeUsedIcons(array|bool $oldIcons = true, array|bool $newIcons = true):array
+	{
+		$set = false;
+		if ($oldIcons === true) {
+			$oldIcons = $this->usedIcons;
+			$set = true;
+		}
+		if ($newIcons === true) {
+			$history = $this->getVersionHistory();
+			$newIcons = (count($history) > 0) ? $history[0]['iconList'] : [];
+		}
+		foreach ($newIcons as $style => $icons) {
+			if (!isset($oldIcons[$style])) {
+				//Style  doesn't exist in previous set, add the whole thing
+				$oldIcons[$style] = $icons;
+			} else {
+				$oldIcons[$style] = array_unique(
+					array_merge($oldIcons[$style], $icons)
+				);
+			}
+		}
+		if ($set) {
+			$this->usedIcons = $oldIcons;
+			update_option(BASE.'usedIcons', $oldIcons);
+		}
+		return $oldIcons;
+	}
+
 	protected function minifyCSS(string $css): string
 	{
 		// Remove comments
@@ -428,7 +535,123 @@
 	public function clearIconCache(): void
 	{
 		delete_option(BASE . 'icon_usage_list'); // Clear DB option
+		delete_option(BASE.'usedIcons');
+		delete_option(BASE.'includeIcons');
+		delete_option(BASE.'iconMap');
 		$this->cache->delete('icon_styles_css');
 		CacheManager::updateTimestamp('icons');
 	}
+
+	protected function archiveCurrentVersion(string $css): void
+	{
+		$history = $this->getVersionHistory();
+
+		$icon_count = 0;
+		foreach ($this->usedIcons as $style => $icons) {
+			$icon_count += count($icons);
+		}
+
+		$newEntry = [
+			'css' => $css,
+			'iconList' => $this->usedIcons,
+			'timestamp' => time(),
+			'icon_count' => $icon_count,
+			'size' => strlen($css),
+			'size_formatted' => size_format(strlen($css), 2)
+		];
+
+		array_unshift($history, $newEntry);
+
+		if (count($history) > self::MAX_VERSIONS) {
+			$history = array_slice($history, 0, self::MAX_VERSIONS);
+		}
+
+		update_option(BASE.'icon_css_history', $history);
+	}
+
+	public function getVersionHistory(): array
+	{
+		return get_option(BASE.'icon_css_history', []);
+	}
+
+	public function restoreVersion(int $timestamp): bool
+	{
+		$history = $this->getVersionHistory();
+
+		foreach ($history as $entry) {
+			if ($entry['timestamp'] === $timestamp) {
+				$css_path = JVB_DIR . '/assets/css/icons.css';
+
+				// Archive current before restoring
+				$current_css = file_get_contents($css_path);
+				if ($current_css !== false) {
+					$this->archiveCurrentVersion($current_css);
+				}
+
+				// Restore the version
+				if (file_put_contents($css_path, $entry['css']) !== false) {
+					$this->usedIcons = $entry['iconList'];
+					update_option(BASE.'usedIcons', $this->usedIcons);
+					CacheManager::updateTimestamp('icons');
+					return true;
+				}
+
+				return false;
+			}
+		}
+
+		error_log("[IconsManager] Version {$timestamp} not found in history");
+		return false;
+	}
+
+	public function forceRefresh(): void
+	{
+		$this->clearIconCache();
+		update_option(BASE.'icons_needs_update', true);
+		CacheManager::updateTimestamp('icons');
+	}
+
+	public function mergeVersions(array $timestamps): bool
+	{
+		if (empty($timestamps)) {
+			return false;
+		}
+
+		$history = get_option(BASE.'icon_css_history', []);
+		$merged_icons = [];
+		// Collect icons from selected versions
+		foreach ($history as $entry) {
+			if (in_array($entry['timestamp'], $timestamps)) {
+				foreach ($entry['iconList'] as $style => $icons) {
+					if (!isset($merged_icons[$style])) {
+						$merged_icons[$style] = [];
+					}
+					// Merge and keep unique
+					$merged_icons[$style] = array_unique(
+						array_merge($merged_icons[$style], $icons)
+					);
+				}
+			}
+		}
+
+		if (empty($merged_icons)) {
+			error_log('[IconsManager] No icons found in selected versions');
+			return false;
+		}
+
+		// Archive current version
+		$current_css = file_get_contents(JVB_DIR . '/assets/css/icons.css');
+		if ($current_css !== false) {
+			$this->archiveCurrentVersion($current_css);
+		}
+
+		// Update used icons and regenerate
+		$this->usedIcons = $merged_icons;
+		update_option(BASE.'usedIcons', $this->usedIcons);
+
+		// Force regeneration
+		$this->regenerateCSS();
+
+		return true;
+	}
 }

--
Gitblit v1.10.0