feat: wire Umami events in builder page (build_started, part_selected, affiliate_link_clicked, build_shared, build_saved)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 06:50:44 -04:00
parent 754a378782
commit 3a345b6c26
+31 -4
View File
@@ -28,6 +28,7 @@ import { X, Link2, Square, CheckSquare } from "lucide-react";
import { useApi } from "@/lib/api";
import { useAuth } from "@/context/AuthContext";
import { trackBuildStarted, trackPartSelected, trackAffiliateLinkClicked, trackBuildShared, trackBuildSaved } from "@/lib/analytics";
import { CATEGORIES } from "@/data/gunbuilderParts";
import { BUILDER_SLOTS, isSlotSatisfied } from "@/data/builderSlots";
@@ -369,20 +370,33 @@ function GunbuilderPageContent() {
useEffect(() => {
if (typeof window === "undefined") return;
// Hydrate persisted build state only. URL actions (platform/select/remove)
// are handled by the dedicated useSearchParams effect later in this file.
// Intentional: Object.keys(...).length > 0 tightens the original condition.
// Previously, an empty {} from localStorage would call setBuild(normalizeBuildState({})),
// which is a no-op. The new check skips that call, which is safe and correct.
let hasBuild = false;
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
const parsed = JSON.parse(stored);
if (parsed && typeof parsed === "object") {
if (parsed && typeof parsed === "object" && Object.keys(parsed).length > 0) {
setBuild(normalizeBuildState(parsed));
hasBuild = true;
}
} catch {
// ignore
}
}
// Fire build_started only for genuinely new builds:
// - No existing parts in localStorage
// - No ?select= param (share-link arrivals have empty localStorage but are not starting fresh)
// Use window.location.search directly: this effect runs at mount before any router
// processing, making window.location.search the authoritative source of the landing URL.
const hasSelectParam = new URLSearchParams(window.location.search).has("select");
if (!hasBuild && !hasSelectParam) {
trackBuildStarted();
}
// Signal that hydration is complete - this allows URL param processing to run
setIsHydrated(true);
}, []);
@@ -842,6 +856,7 @@ function GunbuilderPageContent() {
type: "success",
message: "Saved to your Vault ✅ ",
});
trackBuildSaved();
// Close modal + reset inputs for next variant
setSaveAsOpen(false);
@@ -889,8 +904,10 @@ function GunbuilderPageContent() {
}
return next;
});
const partName = parts.find((p) => String(p.id) === String(partId))?.name ?? String(partId);
trackPartSelected(categoryId, partName, String(partId));
},
[build]
[build, parts]
);
// Handle URL query parameters (?select, ?remove) and keep platform synced + canonical
useEffect(() => {
@@ -1055,6 +1072,7 @@ function GunbuilderPageContent() {
try {
await navigator.clipboard.writeText(shareUrl);
trackBuildShared();
setShareStatus("Shareable link copied to clipboard.");
} catch {
setShareStatus("Could not copy link to clipboard.");
@@ -1074,6 +1092,7 @@ function GunbuilderPageContent() {
text: "Check out my Battl Build.",
url: shareUrl,
});
trackBuildShared(); // fires only on successful share, not on cancel
setShareStatus("Share dialog opened.");
} catch {
setShareStatus(
@@ -1704,6 +1723,14 @@ function GunbuilderPageContent() {
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-md border border-amber-600 bg-amber-500 px-3 py-1.5 text-xs font-semibold text-black hover:bg-amber-400 transition-colors"
onClick={() => {
try {
const retailer = new URL(selectedPart.affiliateUrl).hostname.replace(/^www\./, "");
trackAffiliateLinkClicked(selectedPart.name, retailer);
} catch {
// ignore malformed URLs — if affiliateUrl is malformed, the link href is also broken
}
}}
>
Buy
</a>