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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user