From 3aada9949d51024a92a8b5c6cb70d12f9c3cac16 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 21 Dec 2025 19:59:48 +0000
Subject: [PATCH] =auth refactored via rest, referral system set up for Jane, some javascript consolidation

---
 assets/js/concise/UtilityFunctions.js |  312 ++++++++++++++++++---------------------------------
 1 files changed, 111 insertions(+), 201 deletions(-)

diff --git a/assets/js/dash/UtilityFunctions.js b/assets/js/concise/UtilityFunctions.js
similarity index 77%
rename from assets/js/dash/UtilityFunctions.js
rename to assets/js/concise/UtilityFunctions.js
index 566e1b1..3b9e355 100644
--- a/assets/js/dash/UtilityFunctions.js
+++ b/assets/js/concise/UtilityFunctions.js
@@ -19,7 +19,8 @@
 	}
 }
 /**
- * Format a time value to "X time ago" format
+ * Format a time value as relative time (past or future)
+ * Handles both "X time ago" and "in X time" formats
  *
  * @param {string|Date} dateStr Date to format
  * @returns {string} Formatted time string
@@ -27,60 +28,47 @@
 window.formatTimeAgo = function(dateStr) {
 	const date = dateStr instanceof Date ? dateStr : new Date(dateStr);
 	const now = new Date();
-	const seconds = Math.floor((now - date) / 1000);
+	const diffMs = date - now;
+	const isPast = diffMs < 0;
+
+	// Work with absolute values for calculations
+	const seconds = Math.floor(Math.abs(diffMs) / 1000);
 	const minutes = Math.floor(seconds / 60);
 	const hours = Math.floor(minutes / 60);
 	const days = Math.floor(hours / 24);
 
-	if (hours < 24) {
+	// Just now (within 1 minute either way)
+	if (minutes === 0) {
+		return 'Just now';
+	}
+
+	// Format the time components
+	let timeStr = '';
+
+	if (seconds < 10) {
+		timeStr = 'a moment';
+	} else if (seconds < 60) {
+		timeStr = 'less than a minute'
+	} else if (minutes < 5) {
+		timeStr = 'a few minutes';
+	} else if (hours < 24) {
 		if (hours === 0) {
-			return minutes === 0 ? 'Just now' : `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`;
+			// Minutes only
+			timeStr = `${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`;
+		} else {
+			// Hours
+			timeStr = `${hours} ${hours === 1 ? 'hour' : 'hours'}`;
 		}
-		return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`;
+	} else if (days < 7) {
+		// Days
+		timeStr = `${days} ${days === 1 ? 'day' : 'days'}`;
+	} else {
+		// More than a week - just show the date
+		return date.toLocaleDateString();
 	}
 
-	if (days < 7) {
-		return `${days} ${days === 1 ? 'day' : 'days'} ago`;
-	}
-
-	return date.toLocaleDateString();
-}
-
-/**
- * Format a future date for display
- *
- * @param {string|Date} dateStr Future date
- * @returns {string} Formatted string
- */
-window.formatTimeSoon = function(dateStr) {
-	const date = dateStr instanceof Date ? dateStr : new Date(dateStr);
-	const now = new Date();
-
-	// Handle past dates
-	if (date <= now) {
-		return "Just now";
-	}
-
-	const seconds = Math.floor((date - now) / 1000);
-	const minutes = Math.floor(seconds / 60);
-
-	if (seconds < 60) {
-		return "In a moment";
-	}
-
-	if (minutes < 5) {
-		return "In a few minutes";
-	}
-
-	if (minutes < 20) {
-		return "Coming up soon";
-	}
-
-	if (minutes < 60) {
-		return "In about half an hour";
-	}
-
-	return "Later today";
+	// Add appropriate prefix/suffix based on past or future
+	return isPast ? `${timeStr} ago` : `in ${timeStr}`;
 }
 
 /**
@@ -136,55 +124,6 @@
 }
 
 /**
- * Formats vote from template
- * @param item
- * @param status
- * @returns {Node|ActiveX.IXMLDOMNode|boolean}
- */
-window.formatVote = function(item, status) {
-	let vote = window.getTemplate('voteButton');
-
-	vote.dataset.itemId = item.id;
-	vote.dataset.content = item.content;
-	let up =vote.querySelector('button.up');
-	let down =vote.querySelector('button.down');
-
-	if(status === 'up'){
-		up.classList.add('voted');
-	}
-	if(status === 'down'){
-		down.classList.add('voted');
-	}
-	if(item.upvotes > 0){
-		up.querySelector('.count').textContent = item.upvotes;
-	}
-	if(item.downvotes > 0){
-		down.querySelector('.count').textContent = '-'+item.downvotes;
-	}
-
-	return vote;
-}
-
-
-/**
- * Tests if user has voted for this item
- * @param content
- * @param id
- * @returns {string}
- */
-window.checkVoteStatus = function(content, id){
-	if(!jvbSettings.currentUser){
-		return '';
-	}
-	let status = '';
-	if(window.userVotes && window.userVotes[content]?.has(id)){
-		status = window.userVotes[content].get(id);
-	}
-
-	return status;
-}
-
-/**
  * Gets a clone of an icon element if it exists for efficient DOM manipulation
  * @param icon
  * @returns {Node | ActiveX.IXMLDOMNode}
@@ -211,15 +150,6 @@
 }
 
 /**
- * Tests for empty object
- * @param obj
- * @returns {boolean}
- */
-window.isEmptyObject = function(obj) {
-	return Object.keys(obj).length === 0;
-}
-
-/**
  * Format a number with comma separator (e.g., 1,234)
  * @param {number} num - Number to format
  * @returns {string} - Formatted number
@@ -261,17 +191,6 @@
 }
 
 /**
- * Truncate text to a specific length with ellipsis
- * @param {string} text - Text to truncate
- * @param {number} length - Maximum length
- * @returns {string} - Truncated text
- */
-window.truncateText = function(text, length = 100) {
-	if (!text || text.length <= length) return text;
-	return text.substring(0, length) + '...';
-}
-
-/**
  * Should be faster than setting innerHTML = ''
  * @param node
  */
@@ -296,7 +215,7 @@
 
 	// If same day, just show one date
 	if (start.toDateString() === end.toDateString()) {
-		return start.toLocaleDateString('en-US', {
+		return start.toLocaleDateString('en-CA', {
 			year: 'numeric',
 			month: 'short',
 			day: 'numeric'
@@ -305,43 +224,16 @@
 
 	// If same month and year, show range with month once
 	if (start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear()) {
-		return `${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${end.getDate()}, ${end.getFullYear()}`;
+		return `${start.toLocaleDateString('en-CA', { month: 'short', day: 'numeric' })} - ${end.getDate()}, ${end.getFullYear()}`;
 	}
 
 	// If same year, show full range with year once
 	if (start.getFullYear() === end.getFullYear()) {
-		return `${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${end.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}, ${end.getFullYear()}`;
+		return `${start.toLocaleDateString('en-CA', { month: 'short', day: 'numeric' })} - ${end.toLocaleDateString('en-CA', { month: 'short', day: 'numeric' })}, ${end.getFullYear()}`;
 	}
 
 	// Different years, show full dates
-	return `${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} - ${end.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
-}
-
-/**
- * Debounce function to limit frequent calls
- * @param {Function} func - Function to debounce
- * @param {number} wait - Wait time in milliseconds
- * @returns {Function} - Debounced function
- */
-window.debounce = function(func, wait = 300) {
-	let timeout;
-	return function(...args) {
-		clearTimeout(timeout);
-		timeout = setTimeout(() => func.apply(this, args), wait);
-	};
-}
-
-window.throttle = function(func, limit) {
-	let inThrottle;
-	return function() {
-		const args = arguments;
-		const context = this;
-		if (!inThrottle) {
-			func.apply(context, args);
-			inThrottle = true;
-			setTimeout(() => inThrottle = false, limit);
-		}
-	}
+	return `${start.toLocaleDateString('en-CA', { month: 'short', day: 'numeric', year: 'numeric' })} - ${end.toLocaleDateString('en-CA', { month: 'short', day: 'numeric', year: 'numeric' })}`;
 }
 
 
@@ -737,60 +629,6 @@
 	return !isNaN(parseFloat(n)) && isFinite(n);
 };
 
-window.handleListField = function (elem, value) {
-	if (!Array.isArray(value)) {
-		elem.remove();
-		return;
-	}
-	let li = elem.querySelector('li');
-	value.forEach((v) => {
-		let l = li.cloneNode(true);
-		l.textContent = v;
-		elem.append(l);
-	});
-	li.remove();
-};
-
-window.handleTextField = function (elem, value) {
-	if (typeof value !== "string") {
-		elem.remove();
-		return;
-	}
-	elem.textContent = value;
-};
-
-window.handleImageField = function (elem, value) {
-	if (!Array.isArray(value) || value === 0) {
-		elem.remove();
-		return;
-	}
-	let img = (elem.tagName === 'IMG') ? elem : elem.querySelector('img');
-	if (!img) {
-		elem.remove();
-		return;
-	}
-	img.alt = value.alt;
-	img.src = value.thumbnail;
-	img.dataset.small = value.small;
-	img.dataset.medium = value.medium;
-	img.dataset.large = value.full;
-};
-
-window.handleGalleryField = function (elem, value)
-{
-	if (!Array.isArray(value)) {
-		elem.remove();
-		return;
-	}
-	let img = elem.querySelector('img');
-	value.forEach((v) => {
-		let i = img.cloneNode(true);
-		window.handleImageField(i, v);
-		elem.append(i);
-	});
-	img.remove();
-};
-
 /**
  *
  * @param {object} selectors
@@ -843,3 +681,75 @@
 	}
 }
 window.debouncer = new DebouncedActions();
+
+
+// -----------------------------------------------------
+// Scroll direction + scroll progress
+// -----------------------------------------------------
+const body = document.body;
+const docEl = document.documentElement;
+const progressBar = document.querySelector('.scroll-progress .bar');
+
+let lastY = window.scrollY || docEl.scrollTop || 0;
+let direction = -1;
+let ticking = false;
+let maxScroll = 0;
+
+function updateMaxScroll() {
+	maxScroll = Math.max(0, docEl.scrollHeight - window.innerHeight);
+}
+
+function updateScrollProgress(y) {
+	if (!progressBar) return;
+
+	const progress = maxScroll > 0 ? y / maxScroll : 0;
+	const clamped = Math.max(0, Math.min(1, progress));
+
+	progressBar.style.transform = `scaleX(${clamped})`;
+}
+
+function onScrollFrame() {
+	const y = window.scrollY || docEl.scrollTop || 0;
+
+	// Direction: 1 = down, -1 = up, keep existing if no movement
+	if (y > lastY) {
+		direction = 1;
+	} else if (y < lastY) {
+		direction = -1;
+	}
+
+	lastY = y;
+
+	// Only add scroll-up when actually below top & moving up
+	document.body.classList.toggle('scroll-up', direction < 0 && y > 0);
+
+	// Update progress bar
+	updateScrollProgress(y);
+
+	ticking = false;
+}
+
+// Throttled scroll listener
+window.addEventListener(
+	'scroll',
+	() => {
+		if (!ticking) {
+			ticking = true;
+			requestAnimationFrame(onScrollFrame);
+		}
+	},
+	{ passive: true }
+);
+
+// Debounced resize to recalc scrollable height
+window.addEventListener('resize', () => {
+	window.debouncer.schedule('recalc-max-scroll', () => {
+		updateMaxScroll();
+		updateScrollProgress(window.scrollY || docEl.scrollTop || 0);
+	}, 20);
+});
+
+// Initial setup
+updateMaxScroll();
+updateScrollProgress(lastY);
+

--
Gitblit v1.10.0