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).
89 lines
2.8 KiB
TypeScript
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;
|
|
}
|
|
}
|