main.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
  2. let debounceTimeout;
  3. let timesDebounced = 0;
  4. return function () {
  5. if (timesDebounced == maxDebounceTimes) {
  6. clearTimeout(debounceTimeout);
  7. timesDebounced = 0;
  8. callback();
  9. return;
  10. }
  11. clearTimeout(debounceTimeout);
  12. timesDebounced++;
  13. debounceTimeout = setTimeout(() => {
  14. timesDebounced = 0;
  15. callback();
  16. }, debounceDelay);
  17. };
  18. };
  19. async function fetchPageContents (pageSlug) {
  20. // TODO: handle non 200 status codes/time outs
  21. // TODO: add retries
  22. const response = await fetch(`/api/pages/${pageSlug}/content/`);
  23. const content = await response.text();
  24. return content;
  25. }
  26. function setupCarousels() {
  27. const carouselElements = document.getElementsByClassName("carousel-container");
  28. for (let i = 0; i < carouselElements.length; i++) {
  29. const carousel = carouselElements[i];
  30. carousel.classList.add("show-right-cutoff");
  31. const itemsContainer = carousel.getElementsByClassName("carousel-items-container")[0];
  32. const determineSideCutoffs = () => {
  33. if (itemsContainer.scrollLeft != 0) {
  34. carousel.classList.add("show-left-cutoff");
  35. } else {
  36. carousel.classList.remove("show-left-cutoff");
  37. }
  38. if (Math.ceil(itemsContainer.scrollLeft) + itemsContainer.clientWidth < itemsContainer.scrollWidth) {
  39. carousel.classList.add("show-right-cutoff");
  40. } else {
  41. carousel.classList.remove("show-right-cutoff");
  42. }
  43. }
  44. const determineSideCutoffsRateLimited = throttledDebounce(determineSideCutoffs, 20, 100);
  45. itemsContainer.addEventListener("scroll", determineSideCutoffsRateLimited);
  46. document.addEventListener("resize", determineSideCutoffsRateLimited);
  47. determineSideCutoffs();
  48. }
  49. }
  50. const minuteInSeconds = 60;
  51. const hourInSeconds = minuteInSeconds * 60;
  52. const dayInSeconds = hourInSeconds * 24;
  53. const monthInSeconds = dayInSeconds * 30;
  54. const yearInSeconds = monthInSeconds * 12;
  55. function relativeTimeSince(timestamp) {
  56. const delta = Math.round((Date.now() / 1000) - timestamp);
  57. if (delta < minuteInSeconds) {
  58. return "1m";
  59. }
  60. if (delta < hourInSeconds) {
  61. return Math.floor(delta / minuteInSeconds) + "m";
  62. }
  63. if (delta < dayInSeconds) {
  64. return Math.floor(delta / hourInSeconds) + "h";
  65. }
  66. if (delta < monthInSeconds) {
  67. return Math.floor(delta / dayInSeconds) + "d";
  68. }
  69. if (delta < yearInSeconds) {
  70. return Math.floor(delta / monthInSeconds) + "mo";
  71. }
  72. return Math.floor(delta / yearInSeconds) + "y";
  73. }
  74. function updateRelativeTimeForElements(elements)
  75. {
  76. for (let i = 0; i < elements.length; i++)
  77. {
  78. const element = elements[i];
  79. const timestamp = element.dataset.dynamicRelativeTime;
  80. if (timestamp === undefined)
  81. continue
  82. element.innerText = relativeTimeSince(timestamp);
  83. }
  84. }
  85. function setupDynamicRelativeTime() {
  86. const elements = document.querySelectorAll("[data-dynamic-relative-time]");
  87. const updateInterval = 60 * 1000;
  88. let lastUpdateTime = Date.now();
  89. const updateElementsAndTimestamp = () => {
  90. updateRelativeTimeForElements(elements);
  91. lastUpdateTime = Date.now();
  92. };
  93. const scheduleRepeatingUpdate = () => setInterval(updateElementsAndTimestamp, updateInterval);
  94. if (document.hidden === undefined) {
  95. scheduleRepeatingUpdate();
  96. return;
  97. }
  98. let timeout = scheduleRepeatingUpdate();
  99. document.addEventListener("visibilitychange", () => {
  100. if (document.hidden) {
  101. clearTimeout(timeout);
  102. return;
  103. }
  104. const delta = Date.now() - lastUpdateTime;
  105. if (delta >= updateInterval) {
  106. updateElementsAndTimestamp();
  107. timeout = scheduleRepeatingUpdate();
  108. return;
  109. }
  110. timeout = setTimeout(() => {
  111. updateElementsAndTimestamp();
  112. timeout = scheduleRepeatingUpdate();
  113. }, updateInterval - delta);
  114. });
  115. }
  116. function setupLazyImages() {
  117. const images = document.querySelectorAll("img[loading=lazy]");
  118. if (images.length == 0) {
  119. return;
  120. }
  121. function imageFinishedTransition(image) {
  122. image.classList.add("finished-transition");
  123. }
  124. for (let i = 0; i < images.length; i++) {
  125. const image = images[i];
  126. if (image.complete) {
  127. image.classList.add("cached");
  128. setTimeout(() => imageFinishedTransition(image), 5);
  129. } else {
  130. // TODO: also handle error event
  131. image.addEventListener("load", () => {
  132. image.classList.add("loaded");
  133. setTimeout(() => imageFinishedTransition(image), 500);
  134. });
  135. }
  136. }
  137. }
  138. async function setupPage() {
  139. const pageElement = document.getElementById("page");
  140. const pageContents = await fetchPageContents(pageData.slug);
  141. pageElement.innerHTML = pageContents;
  142. setTimeout(() => {
  143. document.body.classList.add("animate-element-transition");
  144. }, 150);
  145. setTimeout(setupLazyImages, 5);
  146. setupCarousels();
  147. setupDynamicRelativeTime();
  148. }
  149. if (document.readyState === "loading") {
  150. document.addEventListener("DOMContentLoaded", setupPage);
  151. } else {
  152. setupPage();
  153. }