bstein-dev-home/frontend/src/onboarding/useOnboardingGuides.js

115 lines
3.3 KiB
JavaScript
Raw Normal View History

import { ref } from "vue";
import { parseManifest } from "./onboardingGuides";
/**
* Manage onboarding guide media, pagination, and lightbox state.
*
* @param {object} gates - Step completion/blocking predicates from the onboarding flow.
* @returns {object} guide state and guide UI helpers.
*/
export function useOnboardingGuides({ isStepDone, isStepBlocked }) {
const guideShots = ref({});
const guidePage = ref({});
const lightboxShot = ref(null);
/**
* Return screenshot groups for a step, honoring configured head/tail limits.
*
* @param {object} step - Onboarding step definition with optional guide metadata.
* @returns {Array<object>} Screenshot groups to render for the guide carousel.
*/
function guideGroups(step) {
if (!step.guide) return [];
const service = step.guide.service;
const stepKey = step.guide.step;
const serviceShots = guideShots.value?.[service] || {};
const stepShots = serviceShots?.[stepKey] || {};
const groups = Object.values(stepShots);
const take = step.guide.take || step.guide.tail || 0;
if (!take) return groups;
const useTail = Boolean(step.guide.tail);
return groups.map((group) => {
const shots = useTail ? group.shots.slice(-take) : group.shots.slice(0, take);
return { ...group, shots };
});
}
function guideKey(step, group) {
const service = step.guide?.service || "unknown";
const stepKey = step.guide?.step || "unknown";
return `${service}:${stepKey}:${group.id}`;
}
function guideIndex(step, group) {
const key = guideKey(step, group);
const index = guidePage.value[key] ?? 0;
const maxIndex = Math.max(group.shots.length - 1, 0);
return Math.min(Math.max(index, 0), maxIndex);
}
function guideSet(step, group, index) {
const key = guideKey(step, group);
const next = Math.min(Math.max(index, 0), group.shots.length - 1);
guidePage.value = { ...guidePage.value, [key]: next };
}
function guidePrev(step, group) {
guideSet(step, group, guideIndex(step, group) - 1);
}
function guideNext(step, group) {
guideSet(step, group, guideIndex(step, group) + 1);
}
function guideShot(step, group) {
return group.shots[guideIndex(step, group)] || {};
}
function shouldOpenGuide(step, section) {
if (!step || !step.guide || !section) return false;
const first = section.steps.find(
(item) => item.guide && !isStepDone(item.id) && !isStepBlocked(item.id),
);
return Boolean(first && first.id === step.id);
}
function openLightbox(shot) {
if (!shot || !shot.url) return;
lightboxShot.value = shot;
}
function closeLightbox() {
lightboxShot.value = null;
}
async function loadGuideShots() {
try {
const resp = await fetch("/media/onboarding/manifest.json", { headers: { Accept: "application/json" } });
if (!resp.ok) return;
const payload = await resp.json();
const files = Array.isArray(payload?.files) ? payload.files : [];
guideShots.value = parseManifest(files);
} catch {
guideShots.value = {};
}
}
return {
guideShots,
guidePage,
lightboxShot,
guideGroups,
guideKey,
guideIndex,
guideSet,
guidePrev,
guideNext,
guideShot,
shouldOpenGuide,
openLightbox,
closeLightbox,
loadGuideShots,
};
}