1
0
Fork 0
polaris-task-force/src/lib/tours/dismissals.ts
Z8MB1E 8b79c1379d feat(tours): add permission-aware guided tours with replay and device opt-out
Pages declare brief tours in code, keyed to stable data-tour anchors. The layout resolves each visitor's access signals (intelligence and logistics qualifications, admin-panel access, feature flags) and step filtering hides what a user cannot reach, mirroring the sidebar. A tour auto-starts the first time a browser visits its page; dismissals are remembered per browser in localStorage. The header gains a replay control for on-demand runs on toured routes, the tour card gains Skip all tours on this device to opt a browser out of auto-starts, and the Account page holds the re-enable toggle. The card and spotlight get a distinct overlay treatment (glow, accent strip, outlined target).
2026-09-30 18:43:21 -04:00

89 lines
2.8 KiB
TypeScript

/**
* Per-browser tour dismissals in localStorage, keyed by tour id. Mirrors the
* announcement dismissal store (browser storage is read during mount and
* written during mount or the explicit dismissal handler; it is never
* accessed during render). Dismissals are permanent per browser: a tour that
* was dismissed never auto-starts again, but the header replay control can
* always run it on demand.
*/
const DISMISSALS_STORAGE_KEY = "ptf:tour:dismissals";
const OPTED_OUT_STORAGE_KEY = "ptf:tours:opted-out";
export type TourDismissals = Record<string, number>;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
export function readTourDismissals(now: number): TourDismissals {
if (typeof window === "undefined") return {};
try {
const raw = window.localStorage.getItem(DISMISSALS_STORAGE_KEY);
if (!raw) return {};
const parsed: unknown = JSON.parse(raw);
if (!isRecord(parsed)) return {};
const dismissals: TourDismissals = {};
for (const [key, value] of Object.entries(parsed)) {
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
dismissals[key] = value;
}
}
return dismissals;
} catch (error) {
if (error instanceof Error) return {};
throw error;
}
}
export function writeTourDismissals(dismissals: TourDismissals, now: number): void {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(DISMISSALS_STORAGE_KEY, JSON.stringify(dismissals));
} catch (error) {
if (error instanceof Error) return;
throw error;
}
}
/** Merges a single dismissal (timestamp = dismissal time) into the stored map. */
export function storeTourDismissal(tourId: string, now: number): void {
const existing = readTourDismissals(now);
writeTourDismissals({ ...existing, [tourId]: now }, now);
}
/** True when the tour has been dismissed by this browser before `now`. */
export function isTourDismissed(tourId: string, now: number): boolean {
return readTourDismissals(now)[tourId] !== undefined;
}
/**
* Site-wide tours opt-out (per browser). When set, tours never auto-start;
* the header replay control remains as the explicit opt-in.
*/
export function isToursOptedOut(): boolean {
if (typeof window === "undefined") return false;
try {
return window.localStorage.getItem(OPTED_OUT_STORAGE_KEY) === "1";
} catch (error) {
if (error instanceof Error) return false;
throw error;
}
}
export function setToursOptedOut(optedOut: boolean): void {
if (typeof window === "undefined") return;
try {
if (optedOut) {
window.localStorage.setItem(OPTED_OUT_STORAGE_KEY, "1");
} else {
window.localStorage.removeItem(OPTED_OUT_STORAGE_KEY);
}
} catch (error) {
if (error instanceof Error) return;
throw error;
}
}