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