prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

experience.js (4622B)


      1 import { isEditableTarget } from "./dom.js";
      2 
      3 const experienceHandlers = {
      4   keydown: null,
      5   popstate: null,
      6 };
      7 
      8 export function initExperience() {
      9   const section = document.getElementById("experience");
     10   if (!section) return;
     11 
     12   const cards = Array.from(section.querySelectorAll(".exp-card"));
     13   const timelines = Array.from(section.querySelectorAll(".exp-timeline"));
     14   const rows = Array.from(section.querySelectorAll(".exp-row"));
     15   if (cards.length === 0) return;
     16 
     17   let selectedIndex = 0;
     18 
     19   function experienceIDAt(index) {
     20     return cards[index]?.dataset.id || rows[index]?.dataset.id || "";
     21   }
     22 
     23   function indexFromPath() {
     24     const id = decodeURIComponent(
     25       window.location.pathname.match(/^\/experience\/([^/]+)$/)?.[1] || "",
     26     );
     27     if (!id) return -1;
     28     return cards.findIndex((card) => card.dataset.id === id);
     29   }
     30 
     31   function updateExperienceURL(index) {
     32     const id = experienceIDAt(index);
     33     if (!id) return;
     34 
     35     const path = `/experience/${encodeURIComponent(id)}`;
     36     if (window.location.pathname !== path) {
     37       history.replaceState({ tab: "experience" }, "", path);
     38     }
     39   }
     40 
     41   function scrollSelectionIntoView(index) {
     42     const target = rows[index] || cards[index];
     43     if (!target) return;
     44 
     45     const scroller = section.querySelector(".experience-detail");
     46     if (!scroller || !scroller.contains(target)) {
     47       target.scrollIntoView({
     48         behavior: "smooth",
     49         block: "center",
     50         inline: "nearest",
     51       });
     52       return;
     53     }
     54 
     55     const scrollerRect = scroller.getBoundingClientRect();
     56     const targetRect = target.getBoundingClientRect();
     57     const isAbove = targetRect.top < scrollerRect.top + 12;
     58     const isBelow = targetRect.bottom > scrollerRect.bottom - 12;
     59     if (!isAbove && !isBelow) return;
     60 
     61     const targetTop =
     62       targetRect.top - scrollerRect.top + scroller.scrollTop - 8;
     63     scroller.scrollTo({ top: Math.max(targetTop, 0), behavior: "smooth" });
     64   }
     65 
     66   function select(index, updateURL = true, scrollIntoView = false) {
     67     selectedIndex = index;
     68     cards.forEach((card, i) => card.classList.toggle("selected", i === index));
     69     timelines.forEach((timeline, i) =>
     70       timeline.classList.toggle("active", i === index),
     71     );
     72 
     73     if (scrollIntoView) {
     74       scrollSelectionIntoView(index);
     75     }
     76 
     77     if (updateURL) {
     78       updateExperienceURL(index);
     79     }
     80   }
     81 
     82   cards.forEach((card, i) => {
     83     if (card.dataset.boundHover !== "1") {
     84       card.dataset.boundHover = "1";
     85       card.addEventListener("mouseenter", () => select(i));
     86     }
     87   });
     88 
     89   timelines.forEach((timeline, i) => {
     90     if (timeline.dataset.boundHover !== "1") {
     91       timeline.dataset.boundHover = "1";
     92       timeline.addEventListener("mouseenter", () => select(i));
     93     }
     94   });
     95 
     96   if (experienceHandlers.keydown) {
     97     document.removeEventListener("keydown", experienceHandlers.keydown);
     98   }
     99 
    100   experienceHandlers.keydown = (e) => {
    101     if (!section.isConnected || window.isLightboxOpen?.()) return;
    102     if (isEditableTarget(e.target)) return;
    103 
    104     switch (e.key) {
    105       case "ArrowUp":
    106         e.preventDefault();
    107         if (selectedIndex > 0) select(selectedIndex - 1, true, true);
    108         break;
    109       case "ArrowDown":
    110         e.preventDefault();
    111         if (selectedIndex < cards.length - 1)
    112           select(selectedIndex + 1, true, true);
    113         break;
    114       default:
    115         break;
    116     }
    117   };
    118 
    119   document.addEventListener("keydown", experienceHandlers.keydown);
    120 
    121   if (experienceHandlers.popstate) {
    122     window.removeEventListener("popstate", experienceHandlers.popstate);
    123   }
    124 
    125   experienceHandlers.popstate = () => {
    126     if (!section.isConnected) return;
    127     const index = indexFromPath();
    128     if (index >= 0) {
    129       select(index, false);
    130     }
    131   };
    132 
    133   window.addEventListener("popstate", experienceHandlers.popstate);
    134 
    135   section
    136     .querySelectorAll(".exp-image-hero, .exp-image-thumb")
    137     .forEach((img) => {
    138       if (img.dataset.boundClick === "1") return;
    139       img.dataset.boundClick = "1";
    140 
    141       img.addEventListener("click", () => {
    142         const container = img.closest(".exp-card-images");
    143         const allImages = container
    144           ? Array.from(container.querySelectorAll("img")).map((i) => i.src)
    145           : [];
    146         const startIndex = allImages.indexOf(img.src);
    147         window.openLightbox?.(allImages, startIndex);
    148       });
    149     });
    150 
    151   const selectedCard = section.querySelector(".exp-card.selected") || cards[0];
    152   const initialIndex = Math.max(cards.indexOf(selectedCard), 0);
    153   const pathIndex = indexFromPath();
    154   if (pathIndex >= 0) {
    155     select(pathIndex, false);
    156   } else {
    157     select(initialIndex);
    158   }
    159 }