Jake Vanderwerf
2026-01-06 2c955cebb5f1e01fbdb866b50d296fe9fbd852b8
assets/js/concise/UtilityFunctions.js
@@ -25,7 +25,7 @@
 * @param {string|Date} dateStr Date to format
 * @returns {string} Formatted time string
 */
window.formatTimeAgo = function(dateStr) {
window.formatTimeAgo = function(dateStr, dateFormat = 'default') {
   const date = dateStr instanceof Date ? dateStr : new Date(dateStr);
   const now = new Date();
   const diffMs = date - now;
@@ -57,14 +57,43 @@
         timeStr = `${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`;
      } else {
         // Hours
         timeStr = `${hours} ${hours === 1 ? 'hour' : 'hours'}`;
         timeStr = `about ${hours} ${hours === 1 ? 'hour' : 'hours'}`;
      }
   } else if (days < 7) {
      if (days === 1) {
         return isPast ? 'yesterday' : 'tomorrow';
      }
      timeStr = `about ${days} days`;
      // Days
      timeStr = `${days} ${days === 1 ? 'day' : 'days'}`;
   } else {
      // More than a week - just show the date
      return date.toLocaleDateString();
      // More than a week - show the date based on format
      if (dateFormat === 'default') {
         return date.toLocaleDateString();
      }
      // Parse PHP-style format string
      const formatMap = {
         'Y': date.getFullYear(),
         'y': String(date.getFullYear()).slice(-2),
         'F': date.toLocaleDateString('en-CA', { month: 'long' }),
         'M': date.toLocaleDateString('en-CA', { month: 'short' }),
         'm': String(date.getMonth() + 1).padStart(2, '0'),
         'n': date.getMonth() + 1,
         'd': String(date.getDate()).padStart(2, '0'),
         'j': date.getDate(),
         'D': date.toLocaleDateString('en-CA', { weekday: 'short' }),
         'l': date.toLocaleDateString('en-CA', { weekday: 'long' }),
         'H': String(date.getHours()).padStart(2, '0'),
         'i': String(date.getMinutes()).padStart(2, '0'),
         's': String(date.getSeconds()).padStart(2, '0'),
         'h': String(date.getHours() % 12 || 12).padStart(2, '0'),
         'g': date.getHours() % 12 || 12,
         'A': date.getHours() >= 12 ? 'PM' : 'AM',
         'a': date.getHours() >= 12 ? 'pm' : 'am',
      };
      return dateFormat.replace(/[YyFMmnjDlHishgAa]/g, match => formatMap[match]);
   }
   // Add appropriate prefix/suffix based on past or future
@@ -360,17 +389,23 @@
 * @returns {Promise<unknown>}
 */
window.typeText = function(container, text, speed = 50) {
   container.classList.add('typeText');
   return new Promise((resolve) => {
      // Cancel any existing animation on this container
      if (container._typeInterval) {
         clearInterval(container._typeInterval);
         delete container._typeInterval;
      }
      let index = 0;
      container.textContent = '';
      const interval = setInterval(() => {
      container._typeInterval = setInterval(() => {
         if (index < text.length) {
            container.textContent += text.charAt(index);
            index++;
         } else {
            clearInterval(interval);
            clearInterval(container._typeInterval);
            delete container._typeInterval;
            resolve();
         }
      }, speed);
@@ -378,22 +413,29 @@
}
/**
 * Erases text like a keyboard would. TODO: erase a set word from existing text
 * Erases text like a keyboard would.
 * @param container
 * @param speed
 * @returns {Promise<unknown>}
 */
window.eraseText = function(container, speed = 10) {
   return new Promise((resolve) => {
      // Cancel any existing animation on this container
      if (container._eraseInterval) {
         clearInterval(container._eraseInterval);
         delete container._eraseInterval;
      }
      let text = container.textContent;
      let index = text.length;
      const interval = setInterval(() => {
      container._eraseInterval = setInterval(() => {
         if (index > 0) {
            index--;
            container.textContent = text.substring(0, index);
         } else {
            clearInterval(interval);
            clearInterval(container._eraseInterval);
            delete container._eraseInterval;
            resolve();
         }
      }, speed);
@@ -411,31 +453,47 @@
 * @returns {Function} - Call this function to stop the loop
 */
window.typeLoop = function(container, text, typeSpeed = 50, eraseSpeed = 10, pauseAfterType = 1000, pauseAfterErase = 250) {
   // Generate unique key for this container
   const containerId = container.id || container.dataset.typeKey || `type-${Date.now()}`;
   if (!container.dataset.typeKey) {
      container.dataset.typeKey = containerId;
   }
   // Stop any existing loop immediately
   if (container._stopTyping) {
      container._stopTyping();
   }
   let isRunning = true;
   async function loop() {
      while (isRunning) {
         // Type the text
         await window.typeText(container, text, typeSpeed);
         // Wait 1 second
         if (!isRunning) break;
         await new Promise(resolve => setTimeout(resolve, pauseAfterType));
         // Erase the text
         if (!isRunning) break;
         await window.eraseText(container, eraseSpeed);
         // Wait 0.25 seconds before next iteration
         if (!isRunning) break;
         await new Promise(resolve => setTimeout(resolve, pauseAfterErase));
      }
   }
   // Start the loop
   loop();
   // Return a function to stop the loop
   return function stopLoop() {
   const stopLoop = function() {
      isRunning = false;
      if (container._typeInterval) {
         clearInterval(container._typeInterval);
         delete container._typeInterval;
      }
      if (container._eraseInterval) {
         clearInterval(container._eraseInterval);
         delete container._eraseInterval;
      }
   };
   container._stopTyping = stopLoop;
   loop(); // Start immediately
   return stopLoop;
};
window.toCamelCase = function (string) {