All files / src/scripts city-default.ts

100% Statements 23/23
100% Branches 17/17
100% Functions 6/6
100% Lines 20/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105            1x 1x     9x   6x         3x 3x   1x         3x 3x 1x                       8x 7x 7x 5x         2x   3x                 8x 8x           6x   2x                                                                          
// The city a visitor picks (a home hero city tab, or one City filter value on
// /pros or /projects) becomes the default City filter when they next open a
// bare /pros or /projects. The URL stays the truth: the remembered city only
// fills in a listing URL that has no filters at all, and removing the City
// filter forgets it. Crawlers keep no storage, so they never see a redirect.
 
const KEY = "ir-city";
const LISTINGS = new Set(["/pros", "/projects"]);
 
function cityIds(url: URL): string[] {
	return url.searchParams
		.getAll("cityIds")
		.flatMap((v) => v.split(","))
		.filter(Boolean);
}
 
export function rememberedCity(): string | null {
	try {
		return localStorage.getItem(KEY);
	} catch {
		return null;
	}
}
 
export function rememberCity(id: string | null): void {
	try {
		if (id) localStorage.setItem(KEY, id);
		else localStorage.removeItem(KEY);
	} catch {
		// Storage blocked (private mode): no default, nothing else changes.
	}
}
 
/**
 * What arriving at `to` says about the visitor's city: an id (one city
 * picked), null (the City filter was just removed on the same listing), or
 * undefined (nothing to change).
 */
export function cityChoice(from: URL, to: URL): string | null | undefined {
	if (!LISTINGS.has(to.pathname)) return undefined;
	const ids = cityIds(to);
	if (ids.length === 1) return ids[0];
	if (
		ids.length === 0 &&
		from.pathname === to.pathname &&
		cityIds(from).length > 0
	) {
		return null;
	}
	return undefined;
}
 
/** The bare listing URL with the remembered city filled in, or null. */
export function withDefaultCity(
	href: string,
	city: string | null,
	allowed: string[],
): string | null {
	const url = new URL(href);
	if (
		!LISTINGS.has(url.pathname) ||
		url.search ||
		!city ||
		!allowed.includes(city)
	) {
		return null;
	}
	return `${url.pathname}?cityIds=${encodeURIComponent(city)}`;
}
 
/* v8 ignore start -- browser wiring; the decisions above are unit-tested */
if (typeof document !== "undefined") {
	// Hero city tabs carry data-city-id.
	document.addEventListener(
		"click",
		(e) => {
			const tab = (e.target as Element | null)?.closest?.("a[data-city-id]");
			if (tab) rememberCity(tab.getAttribute("data-city-id"));
		},
		true,
	);
 
	document.addEventListener("astro:before-preparation", (e) => {
		const { from, to } = e as Event & { from: URL; to: URL };
		const choice = cityChoice(from, to);
		if (choice !== undefined) rememberCity(choice);
	});
 
	document.addEventListener("astro:page-load", () => {
		const here = new URL(location.href);
		const choice = cityChoice(here, here);
		if (choice) rememberCity(choice);
 
		const allowed = Array.from(
			document.querySelectorAll(
				'details[data-facet-param="cityIds"] [data-facet-option]',
			),
		).map((el) => el.getAttribute("data-facet-option") ?? "");
		const target = withDefaultCity(location.href, rememberedCity(), allowed);
		// replace(): the bare URL never becomes a Back stop that bounces again.
		if (target) location.replace(target);
	});
}
/* v8 ignore stop */