feat(FN-297): add social proof impression tracking to landing page testimonials (+2 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled

Commits merged:
- docs(FN-297): update FN-275 audit with social proof event locations
- fix(FN-297): format IntersectionObserver mock in test file
- feat(FN-297): add social proof impression tracking to landing page testimonials

Files changed:
apps/web/src/routes/__tests__/index.test.tsx |   7 ++
 apps/web/src/routes/index.tsx                | 111 +++++++++++++++++++++++++--
 docs/product/funnel-audit-p1-cro-2026-05.md  |  19 ++++-
 3 files changed, 127 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-297
This commit is contained in:
Fusion
2026-05-13 07:28:25 +00:00
parent 80596a4ceb
commit 3dd4dc2f77
3 changed files with 127 additions and 10 deletions

View File

@@ -71,6 +71,13 @@ import { useAuthStore } from "@/stores/auth.store";
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal(
"IntersectionObserver",
vi.fn().mockImplementation(() => ({
observe: vi.fn(),
disconnect: vi.fn(),
})),
);
});
test("captures vin_decode_success on successful decode", async () => {

View File

@@ -25,7 +25,7 @@ import {
X,
Zap,
} from "lucide-react";
import { Suspense, lazy, useEffect, useState } from "react";
import { Suspense, lazy, useEffect, useRef, useState } from "react";
// Remotion — lazy loaded to keep initial bundle lean
const RemotionSchemaPlayer = lazy(() =>
@@ -553,6 +553,69 @@ export function HomePage() {
setVin("WVWZZZ1JZ3W597935");
};
// ─── Social proof impression tracking ────────────────────────────────────
const heroBadgeRef = useRef<HTMLDivElement>(null);
const testimonialStripRef = useRef<HTMLDivElement>(null);
const testimonialsRef = useRef<HTMLElement>(null);
const heroBadgeFiredRef = useRef(false);
const testimonialStripFiredRef = useRef(false);
const testimonialsFiredRef = useRef(false);
useEffect(() => {
const el = heroBadgeRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !heroBadgeFiredRef.current) {
heroBadgeFiredRef.current = true;
capture("social_proof_impression", { page: "landing", section: "hero_badge" });
}
},
{ threshold: 0.5 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
useEffect(() => {
const el = testimonialStripRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !testimonialStripFiredRef.current) {
testimonialStripFiredRef.current = true;
capture("social_proof_impression", { page: "landing", section: "testimonial_strip" });
}
},
{ threshold: 0.5 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
useEffect(() => {
const el = testimonialsRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !testimonialsFiredRef.current) {
testimonialsFiredRef.current = true;
capture("social_proof_impression", { page: "landing", section: "testimonials" });
}
},
{ threshold: 0.5 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
function handleSocialProofEngaged(section: string) {
capture("social_proof_engaged", { page: "landing", section });
}
return (
<div className="min-h-screen bg-background text-foreground">
{/* ─── 1. STICKY HEADER ─────────────────────────────────────────── */}
@@ -708,7 +771,20 @@ export function HomePage() {
</div>
{/* Social proof badge */}
<div className="mt-3 inline-flex items-center gap-3 rounded-full border border-border bg-muted px-4 py-1.5 text-xs text-muted-foreground">
<div
ref={heroBadgeRef}
// biome-ignore lint/a11y/useSemanticElements: analytics-only interaction on content block
role="button"
tabIndex={0}
onClick={() => handleSocialProofEngaged("hero_badge")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSocialProofEngaged("hero_badge");
}
}}
className="mt-3 inline-flex cursor-pointer items-center gap-3 rounded-full border border-border bg-muted px-4 py-1.5 text-xs text-muted-foreground"
>
<div className="flex -space-x-2">
{["AY", "MK", "ÖD", "BT"].map((initials) => (
<div
@@ -889,7 +965,10 @@ export function HomePage() {
</section>
{/* ─── 3.5. TESTIMONIAL STRIP ────────────────────────────────── */}
<section className="border-b border-border bg-background px-4 py-14 sm:px-6">
<section
ref={testimonialStripRef}
className="border-b border-border bg-background px-4 py-14 sm:px-6"
>
<div className="mx-auto max-w-7xl">
<div className="text-center">
<p className="text-sm text-muted-foreground">Kullanıcılarımız ne diyor?</p>
@@ -915,7 +994,17 @@ export function HomePage() {
].map((t) => (
<div
key={t.name}
className="rounded-2xl border border-border bg-surface px-6 py-5 transition-all duration-300 hover:-translate-y-0.5 hover:border-foreground/10"
// biome-ignore lint/a11y/useSemanticElements: analytics-only interaction on content block
role="button"
tabIndex={0}
onClick={() => handleSocialProofEngaged("testimonial_strip")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSocialProofEngaged("testimonial_strip");
}
}}
className="cursor-pointer rounded-2xl border border-border bg-surface px-6 py-5 transition-all duration-300 hover:-translate-y-0.5 hover:border-foreground/10"
>
<div className="flex gap-0.5">
{dynamicKeys(5).map((k) => (
@@ -1358,7 +1447,7 @@ export function HomePage() {
</section>
{/* ─── 9.6. TESTIMONIALS ────────────────────────────────────── */}
<section className="bg-surface-alt px-4 py-20 sm:px-6">
<section ref={testimonialsRef} className="bg-surface-alt px-4 py-20 sm:px-6">
<div className="mx-auto max-w-7xl">
<div className="text-center">
<SectionBadge>Müşteri Yorumları</SectionBadge>
@@ -1371,7 +1460,17 @@ export function HomePage() {
{TESTIMONIALS.map((t) => (
<div
key={t.name}
className="group rounded-2xl border border-border bg-surface p-6 transition-all duration-300 hover:-translate-y-1 hover:border-foreground/20 hover:shadow-[var(--shadow-md)]"
// biome-ignore lint/a11y/useSemanticElements: analytics-only interaction on content block
role="button"
tabIndex={0}
onClick={() => handleSocialProofEngaged("testimonials")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSocialProofEngaged("testimonials");
}
}}
className="group cursor-pointer rounded-2xl border border-border bg-surface p-6 transition-all duration-300 hover:-translate-y-1 hover:border-foreground/20 hover:shadow-[var(--shadow-md)]"
>
<div className="flex gap-0.5">
{dynamicKeys(t.rating).map((k) => (