From b5abd615697146beeca6dba4acd057d049554a30 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 02 Jan 2026 00:16:00 +0000
Subject: [PATCH] Merge branch 'main' of https://github.com/jakevdwerf/jvb

---
 src/feed/view.js |  315 +++++++++++++++++++++++++++++++++++----------------
 1 files changed, 214 insertions(+), 101 deletions(-)

diff --git a/src/feed/view.js b/src/feed/view.js
index 5e3b453..7110cad 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -63,15 +63,17 @@
 		};
 		this.ui = window.uiFromSelectors(this.elements);
 
-		this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]');
+
+		this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
 		this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
-		if (this.ui.content.length > 0) {
+		if (this.ui.content && this.ui.content.length > 0) {
 			this.contentTypes = Array.from(
 				this.ui.content
 			).map(content => content.value);
 		} else {
 			this.contentTypes = [this.container.dataset['content']];
 		}
+
 		if (this.ui.taxonomies.length>0) {
 			this.taxonomies = Array.from(
 				this.ui.taxonomies,
@@ -79,6 +81,8 @@
 		} else {
 			this.taxonomies = [];
 		}
+
+
 	}
 
 	async initTaxonomies() {
@@ -176,13 +180,15 @@
 		this.syncUIToFilters();
 	}
 	syncUIToFilters() {
-		// Check radio buttons
-		Object.entries(this.filters).forEach(([key, value]) => {
-			const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
-			if (input) {
-				input.checked = true;
-			}
-		});
+		if (this.ui.filterContainer) {
+			// Check radio buttons
+			Object.entries(this.filters).forEach(([key, value]) => {
+				const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
+				if (input) {
+					input.checked = true;
+				}
+			});
+		}
 
 		// Update content-specific visibility
 		this.updateContentFor(this.filters.content);
@@ -282,7 +288,7 @@
 				this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
 			}
 		});
-		if (hasTaxonomy) {
+		if (this.ui.filterContainer && hasTaxonomy) {
 			for (let [tax, ids] in Object.entries(this.taxonomyFilters)) {
 				let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`);
 				if (button) {
@@ -359,9 +365,6 @@
 				this.removePlaceholders();
 				this.ui.grid.append(fragment);
 
-				// Observe images after adding to DOM
-				this.observeImages(this.ui.grid);
-
 				if (this.config.gallery) {
 					this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
 				}
@@ -377,8 +380,12 @@
 			this.a11y.announceItems(0, this.store.filters['page'] >1, false);
 		}
 
-		this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
-		this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
+		if (this.ui.filters.match) {
+			this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
+		}
+		if (this.ui.clearFilter) {
+			this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
+		}
 	}
 
 	/**
@@ -386,69 +393,171 @@
 	 * @param {object} item
 	 */
 	createItemElement(item) {
-		let template = window.getTemplate('feed-item');
-		if (Object.hasOwn(template.dataset, 'timeline')) {
-			return this.createTimelineElement(item, template);
+		let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
+
+		const isTimeline = Object.hasOwn(template.dataset, 'timeline');
+
+		// Format fields using helpers
+		for (let [fieldName, value] of Object.entries(item.fields)) {
+			if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue;
+			let el = template.querySelector(`[data-field="${fieldName}"]`);
+			if (!el) continue;
+
+			if (value === '') {
+				el.remove();
+				continue;
+			}
+
+			if (this.isImageField(item, value)) {
+				this.formatImageFields(el, value, item);
+			} else if (this.isTaxonomyField(item, fieldName)) {
+				this.formatTaxonomyField(el, item, fieldName, value);
+			} else if (this.isTimeField(el)) {
+				this.formatTimeField(el, value);
+			} else {
+				this.formatField(el, value);
+			}
 		}
+
+		// Handle link
+		let link = template.querySelector('a');
+		if (link && item.url !== '') {
+			[
+				link.href,
+				link.title
+			] = [
+				item.url,
+				`View ${item.fields['post_title']??'Item'}`
+			];
+		}
+
+		if (isTimeline) {
+			this.addTimelineElements(item, template);
+		}
+
 		return template;
 	}
+	splitIDs(value) {
+		return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
+	}
+	isImageField(item, value) {
+		if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) {
+			return false;
+		}
+		let values = this.splitIDs(value);
 
-	createTimelineElement(item, template) {
+		return values.some(v =>
+			Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v))
+		);
+	}
+	formatImageFields(element, value, item) {
+		let values = this.splitIDs(value); // Convert string to array first
+		if (values.length === 0) return;
+
+		if (values.length > 1) {
+			let image = element.querySelector('img');
+			if (!image) return;
+			values.forEach(imgID => {
+				let img = image.cloneNode(true);
+				this.formatImageField(img, imgID, item);
+				element.append(img);
+			});
+			image.remove();
+		} else {
+			if (element.tagName !== 'IMG') {
+				element = element.querySelector('img');
+				if (!element) return;
+			}
+			this.formatImageField(element, values[0], item);
+		}
+	}
+		formatImageField(element, value, item) {
+			let imgData = item.images[value]??false;
+			if (!imgData) return;
+				[
+					element.src,
+					element.srcset,
+					element.alt
+				] = [
+					imgData.tiny,
+					`${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`,
+					imgData['image-alt-text']
+				]
+		}
+	isTaxonomyField(item, field) {
+		if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) {
+			return false;
+		}
+
+		return Object.keys(item.taxonomies).includes(field);
+	}
+	formatTaxonomyField(element, item, field, value) {
+		if (element.tagName !== 'UL' || !element.querySelector('li')) return;
+		let values = this.splitIDs(value);
+		if (values.length === 0) {
+			element.remove();
+		}
+		let listItem = element.querySelector('li');
+		for (let termID of values) {
+			let term = item.taxonomies[field][termID]??false;
+			if (!term) continue;
+			let termItem = listItem.cloneNode(true);
+			let link = termItem.querySelector('a');
+			if (!link) continue;
+
+			[
+				link.href,
+				link.title,
+				link.textContent
+			] = [
+				term.url,
+				`See more ${term.title}`,
+				term.title
+			];
+			element.append(termItem);
+		}
+		listItem.remove();
+	}
+	isTimeField(el) {
+		return el.tagName === 'TIME' || el.querySelector('time') !== null;
+	}
+	formatTimeField(element, value) {
+		if (element.tagName !== 'TIME') {
+			element = element.querySelector('time');
+			if (!element) return;
+		}
+		element.setAttribute('datetime', value);
+		element.textContent = window.formatTimeAgo(value, 'F Y');
+	}
+	formatField(element, value) {
+		element.textContent = value;
+	}
+
+	addTimelineElements(item, template) {
 		let [
-			main,
-			link,
-			beforeImg,
-			afterImg,
-			afterText,
+			afterEl,
+			number,
 			started,
-			lastTreated,
-			total,
-			termList,
-			timeline
+			last
 		] = [
-			template,
-			template.querySelector('a'),
-			template.querySelector('img.before'),
-			template.querySelector('img.after'),
-			template.querySelector('summary span:last-of-type'),
-			template.querySelector('p.started time'),
-			template.querySelector('p.updated time'),
-			template.querySelector('p.total b'),
-			template.querySelector('.term-list'),
-			Object.values(item.fields.order)
+			template.querySelector('span.after-text'),
+			template.querySelector('[data-field="number"] b'),
+			template.querySelector('[data-field="started"] time'),
+			template.querySelector('[data-field="updated"] time')
 		];
-		let numberTreatments = timeline.length - 1;
-		let beforeImgData = item.images[timeline[0]['post_thumbnail']];
-		let afterImgData = item.images[timeline[numberTreatments]['post_thumbnail']];
 
-		[
-			main.dataset.id,
-			link.href,
-			beforeImg.src,
-			beforeImg.dataset.small,
-			beforeImg.dataset.medium,
-			afterImg.src,
-			afterImg.dataset.small,
-			afterImg.dataset.medium,
-			afterText.textContent,
-			started.textContent,
-			lastTreated.textContent,
-			total.textContent
-		] = [
-			item.id,
-			item.url,
-			beforeImgData['tiny'],
-			beforeImgData.small,
-			beforeImgData.medium,
-			afterImgData['tiny'],
-			afterImgData.small,
-			afterImgData.medium,
-			`${afterText.textContent} ${numberTreatments} Tx`,
-			timeline[0].date??item.date,
-			timeline[numberTreatments].date??'',
-			`${numberTreatments} Treatments`
-		];
-		return template;
+		if (afterEl) {
+			afterEl.textContent = `After ${item.fields.number} Tx`;
+		}
+		if (number) {
+			number.textContent = item.fields.number;
+		}
+		if (started) {
+			this.formatTimeField(started, item.fields.timeline[0]['post_date']);
+		}
+		if (last) {
+			this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']);
+		}
 	}
 
 	removePlaceholders() {
@@ -467,7 +576,7 @@
 
 			let rand = Math.floor(Math.random() * total);
 			let icon;
-			if (this.ui.content.length > 0) {
+			if (this.ui.content && this.ui.content.length > 0) {
 				icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true);
 			} else {
 				icon = window.getIcon(this.container.dataset.icon);
@@ -584,35 +693,6 @@
 		document.addEventListener('change', this.changeHandler);
 	}
 
-	loadImage(img) {
-		const src = this.getAppropriateImageSize(img);
-		if (src && src !== img.src) {
-			img.src = src;
-			img.dataset.loaded = 'true';
-		}
-	}
-
-	getAppropriateImageSize(img) {
-		const width = window.innerWidth;
-
-		if (width < 768 && img.dataset.small) {
-			return img.dataset.small;
-		} else if (img.dataset.medium) {
-			return img.dataset.medium;
-		}
-
-		return img.src; // Fallback to current src
-	}
-
-	observeImages(container) {
-		const images = container.querySelectorAll('img[data-small], img[data-medium]');
-		images.forEach(img => {
-			if (!img.dataset.loaded) {
-				this.imageObserver.observe(img);
-			}
-		});
-	}
-
 	handlePopState(e) {
 		if (e.state?.filters) {
 			if (this.processURLFilters()) {
@@ -681,6 +761,39 @@
 	}
 }
 
-document.addEventListener('DOMContentLoaded', function() {
-	window.feedBlock = new FeedBlock();
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe(event => {
+		if (event === 'auth-loaded') {
+			window.feedBlock = new FeedBlock();
+		}
+	});
+
+	let item = {
+		content: "art",
+		date: "2025-12-24 03:37:26",
+		fields: {
+			gallery: "",
+			post_content: "",
+			post_thumbnail: 200,
+			post_title: "Great Gray Owl",
+			price: "",
+		},
+		icon: "arrows-clockwise",
+		id: 195,
+		images: {
+			200: {
+				'image-alt-text': "",
+				'image-caption': "",
+				'image-title': "Great Gray Owl",
+				large: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl.jpg",
+				medium: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-1024x1024.jpg",
+				small: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-300x300.jpg",
+				tiny: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-50x50.jpg"
+			}
+		},
+		url: "http://jakevan.test/art/great-gray-owl/",
+		user_id: 3
+	};
 });
+
+

--
Gitblit v1.10.0