import { computed, onMounted, onUnmounted, shallowRef, watch, type Ref } from 'vue'; import type { PublicAssetManifest, PublicProject } from '@/content/types'; import type { JourneySnapshot } from '@/journey/types'; import { assetUrl } from '@/content/asset-url'; import { routePlate } from '@/lib/route-plate'; import { sectionLocalProgress } from '@/journey/route-log'; import { activeStop, billboardStops, publishBillboard, readBillboard } from '@/journey/billboard-feed'; /** DRIVE-IN, DOM side: html[data-billboard-active] and (fine pointer) html[data-billboard-focus]. After mount only. */ export function useBillboard(journey: Readonly>, list: () => readonly PublicProject[], manifest: PublicAssetManifest, exit: string): void { const stops = shallowRef([]); const html = (): DOMStringMap => document.documentElement.dataset; const top = (el: Element | null | undefined): number => (el?.getBoundingClientRect().top ?? 0) + scrollY; const cases = (): HTMLElement[] => [...document.querySelectorAll('#selected-builds [data-project-id]')]; const measure = (): void => { stops.value = billboardStops(top(document.getElementById('selected-builds-heading')), top(document.getElementById('additional-work-heading')), cases().map((el) => top(el.firstElementChild))); publishBillboard(list().map((p, i) => ({ id: p.id, title: p.title, plate: routePlate(exit, i, p), shots: (p.media ?? []).flatMap((m) => (m.kind === 'video' ? [] : [{ id: m.assetId, url: assetUrl(manifest, m.assetId), kind: m.kind }])) })), stops.value); }; const active = computed(() => list()[activeStop(sectionLocalProgress(journey.value, 'selected-builds'), stops.value, list().length)]?.id ?? null); watch(() => journey.value.sectionActivation.layoutRevision, measure); watch(active, (id) => { if (id === null) delete html().billboardActive; else html().billboardActive = id; for (const el of cases()) el.toggleAttribute('data-billboard-lit', el.dataset.projectId === id); }); let idle = 0; const hover = (event: Event): void => { const src = (event.target as Element).closest?.('.project-media-gallery img')?.getAttribute('src'); const shot = readBillboard().projects.flatMap((p) => p.shots).find((s) => s.url === src); clearTimeout(idle); if (shot !== undefined && event.type === 'pointerover') html().billboardFocus = shot.id; else if (shot !== undefined) idle = window.setTimeout(() => delete html().billboardFocus, 1200); }; const listen = (on: boolean): void => ['pointerover', 'pointerout'].forEach((type) => document.getElementById('selected-builds')?.[on ? 'addEventListener' : 'removeEventListener'](type, hover)); onMounted(() => { measure(); if (window.matchMedia?.('(hover: hover) and (pointer: fine)').matches) listen(true); }); onUnmounted(() => { listen(false); clearTimeout(idle); delete html().billboardActive; delete html().billboardFocus; }); }