test: add weighted taste profile and search query tests
This commit is contained in:
153
static/top_pick_carousel.ts
Normal file
153
static/top_pick_carousel.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
const carouselScrollEpsilon = 2;
|
||||
const fallbackCarouselOverlap = 96;
|
||||
const itemOverlapRatio = 0.45;
|
||||
|
||||
type TopPickCarousel = {
|
||||
track: HTMLElement;
|
||||
previous: HTMLButtonElement;
|
||||
next: HTMLButtonElement;
|
||||
previousFade: HTMLElement;
|
||||
nextFade: HTMLElement;
|
||||
};
|
||||
|
||||
const getTopPickCarousel = (root: HTMLElement): TopPickCarousel | null => {
|
||||
const track = root.querySelector<HTMLElement>("[data-top-pick-track]");
|
||||
const previous = root.querySelector<HTMLButtonElement>("[data-top-pick-prev]");
|
||||
const next = root.querySelector<HTMLButtonElement>("[data-top-pick-next]");
|
||||
const previousFade = root.querySelector<HTMLElement>("[data-top-pick-prev-fade]");
|
||||
const nextFade = root.querySelector<HTMLElement>("[data-top-pick-next-fade]");
|
||||
|
||||
if (!track || !previous || !next || !previousFade || !nextFade) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { track, previous, next, previousFade, nextFade };
|
||||
};
|
||||
|
||||
const topPickCarousels = (): HTMLElement[] =>
|
||||
Array.from(document.querySelectorAll<HTMLElement>("[data-top-pick-carousel]"));
|
||||
|
||||
const maxScrollLeft = (track: HTMLElement): number =>
|
||||
Math.max(0, track.scrollWidth - track.clientWidth);
|
||||
|
||||
const updateTopPickCarousel = (root: HTMLElement): void => {
|
||||
const carousel = getTopPickCarousel(root);
|
||||
if (!carousel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxScroll = maxScrollLeft(carousel.track);
|
||||
const canScroll = maxScroll > carouselScrollEpsilon;
|
||||
const hasPrevious = canScroll && carousel.track.scrollLeft > carouselScrollEpsilon;
|
||||
const hasNext = canScroll && carousel.track.scrollLeft < maxScroll - carouselScrollEpsilon;
|
||||
|
||||
carousel.previous.disabled = false;
|
||||
carousel.next.disabled = false;
|
||||
carousel.previous.classList.toggle("hidden", !hasPrevious);
|
||||
carousel.previous.classList.toggle("inline-flex", hasPrevious);
|
||||
carousel.previous.setAttribute("aria-hidden", String(!hasPrevious));
|
||||
carousel.previous.tabIndex = hasPrevious ? 0 : -1;
|
||||
carousel.previousFade.classList.toggle("hidden", !hasPrevious);
|
||||
|
||||
carousel.next.classList.toggle("hidden", !hasNext);
|
||||
carousel.next.classList.toggle("inline-flex", hasNext);
|
||||
carousel.next.setAttribute("aria-hidden", String(!hasNext));
|
||||
carousel.next.tabIndex = hasNext ? 0 : -1;
|
||||
carousel.nextFade.classList.toggle("hidden", !hasNext);
|
||||
};
|
||||
|
||||
const updateTopPickCarousels = (): void => {
|
||||
topPickCarousels().forEach(updateTopPickCarousel);
|
||||
};
|
||||
|
||||
const carouselScrollAmount = (track: HTMLElement): number => {
|
||||
const firstItem = track.querySelector<HTMLElement>("[data-top-pick-item]");
|
||||
if (!firstItem) {
|
||||
return Math.max(160, track.clientWidth - fallbackCarouselOverlap);
|
||||
}
|
||||
|
||||
const itemWidth = firstItem.getBoundingClientRect().width;
|
||||
const overlap = Math.max(fallbackCarouselOverlap, itemWidth * itemOverlapRatio);
|
||||
|
||||
return Math.max(itemWidth, track.clientWidth - Math.min(itemWidth, overlap));
|
||||
};
|
||||
|
||||
const scrollTopPickCarousel = (root: HTMLElement, direction: -1 | 1): void => {
|
||||
const carousel = getTopPickCarousel(root);
|
||||
if (!carousel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentScroll = carousel.track.scrollLeft;
|
||||
const targetScroll =
|
||||
direction < 0
|
||||
? Math.max(0, currentScroll - carouselScrollAmount(carousel.track))
|
||||
: Math.min(
|
||||
maxScrollLeft(carousel.track),
|
||||
currentScroll + carouselScrollAmount(carousel.track),
|
||||
);
|
||||
|
||||
carousel.track.scrollTo({
|
||||
left: targetScroll,
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
window.setTimeout(() => updateTopPickCarousel(root), 350);
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
(event: MouseEvent): void => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = target.closest("[data-top-pick-prev]");
|
||||
if (previous) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const root = previous.closest<HTMLElement>("[data-top-pick-carousel]");
|
||||
if (root) {
|
||||
scrollTopPickCarousel(root, -1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const next = target.closest("[data-top-pick-next]");
|
||||
if (!next) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const root = next.closest<HTMLElement>("[data-top-pick-carousel]");
|
||||
if (root) {
|
||||
scrollTopPickCarousel(root, 1);
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
"scroll",
|
||||
(event: Event): void => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement) || !target.matches("[data-top-pick-track]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const root = target.closest<HTMLElement>("[data-top-pick-carousel]");
|
||||
if (root) {
|
||||
updateTopPickCarousel(root);
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
document.addEventListener("DOMContentLoaded", updateTopPickCarousels);
|
||||
document.addEventListener("htmx:afterSwap", updateTopPickCarousels);
|
||||
document.addEventListener("htmx:afterSettle", updateTopPickCarousels);
|
||||
window.addEventListener("resize", updateTopPickCarousels);
|
||||
|
||||
updateTopPickCarousels();
|
||||
Reference in New Issue
Block a user