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