refactor tracking to ues callbacks instead of refs

This commit is contained in:
2025-11-12 10:50:00 +01:00
parent fd17ea0620
commit 18d935270f
3 changed files with 143 additions and 134 deletions

View File

@@ -1,73 +1,87 @@
'use client';
import type { EventName } from '@/lib/events';
import { useHoverTracking } from '@/hooks/useHoverTracking';
const dispatchInteraction = (eventName: EventName, productId?: string, metadata?: Record<string, unknown>) => {
const e = new CustomEvent('definedInteraction', {
detail: { eventName, productId, metadata },
});
document.dispatchEvent(e);
const e = new CustomEvent('definedInteraction', {
detail: { eventName, productId, metadata },
});
document.dispatchEvent(e);
};
type CabinClass = 'economy' | 'premium' | 'business' | 'first';
type FareRule = 'flexible' | 'standard' | 'basic';
interface Flight {
id: string;
departure: { time: string; airport: string };
arrival: { time: string; airport: string };
duration: string;
stops: number;
cabinClass: CabinClass;
fareRule: FareRule;
refundable: boolean;
basePrice: number;
id: string;
departure: { time: string; airport: string };
arrival: { time: string; airport: string };
duration: string;
stops: number;
cabinClass: CabinClass;
fareRule: FareRule;
refundable: boolean;
basePrice: number;
}
const PriceDisplay = ({ price }: { price: number }) => (
<div className="fare-price">${price}</div>
<div className="fare-price">${price}</div>
);
export default function AirlineCard({ flight }: { flight: Flight }) {
const handleCardClick = () => {
dispatchInteraction('product_view', flight.id, {
cabinClass: flight.cabinClass,
fareRule: flight.fareRule,
price: flight.basePrice,
const durationRef = useHoverTracking({
eventName: 'hover_over_title',
productId: flight.id,
metadata: { elementText: flight.duration },
});
};
return (
<div
className="flight-card cursor-pointer"
onClick={handleCardClick}
onMouseEnter={() => dispatchInteraction('product_hover', flight.id)}
>
<div className="flight-timing">
<div className="flight-time">{flight.departure.time}</div>
<div className="flight-airport">{flight.departure.airport}</div>
</div>
const priceRef = useHoverTracking({
eventName: 'hover_over_paragraph',
productId: flight.id,
metadata: { elementText: `$${flight.basePrice}` },
});
<div className="flight-route">
<div className="flight-duration">{flight.duration}</div>
<div className="flight-stops">
{flight.stops === 0 ? 'Direct' : `${flight.stops} stop${flight.stops > 1 ? 's' : ''}`}
const handleCardClick = () => {
dispatchInteraction('view_item_page', flight.id, {
cabinClass: flight.cabinClass,
fareRule: flight.fareRule,
price: flight.basePrice,
});
};
return (
<div
className="flight-card cursor-pointer"
onClick={handleCardClick}
>
<div className="flight-timing">
<div className="flight-time">{flight.departure.time}</div>
<div className="flight-airport">{flight.departure.airport}</div>
</div>
<div className="flight-route">
<div ref={durationRef} className="flight-duration">{flight.duration}</div>
<div className="flight-stops">
{flight.stops === 0 ? 'Direct' : `${flight.stops} stop${flight.stops > 1 ? 's' : ''}`}
</div>
</div>
<div className="flight-timing">
<div className="flight-time">{flight.arrival.time}</div>
<div className="flight-airport">{flight.arrival.airport}</div>
</div>
<div className="flight-pricing">
<div className="fare-class capitalize mb-2">{flight.cabinClass}</div>
<div className="text-sm text-[var(--text-secondary)] mb-2 capitalize">{flight.fareRule}</div>
{flight.refundable && (
<div className="badge-value text-xs mb-2">Refundable</div>
)}
<div ref={priceRef}>
<PriceDisplay price={flight.basePrice} />
</div>
</div>
</div>
</div>
<div className="flight-timing">
<div className="flight-time">{flight.arrival.time}</div>
<div className="flight-airport">{flight.arrival.airport}</div>
</div>
<div className="flight-pricing">
<div className="fare-class capitalize mb-2">{flight.cabinClass}</div>
<div className="text-sm text-[var(--text-secondary)] mb-2 capitalize">{flight.fareRule}</div>
{flight.refundable && (
<div className="badge-value text-xs mb-2">Refundable</div>
)}
<PriceDisplay price={flight.basePrice} />
</div>
</div>
);
);
}

View File

@@ -1,87 +1,101 @@
'use client';
import type { EventName } from '@/lib/events';
import { useHoverTracking } from '@/hooks/useHoverTracking';
const dispatchInteraction = (eventName: EventName, productId?: string, metadata?: Record<string, unknown>) => {
const e = new CustomEvent('definedInteraction', {
detail: { eventName, productId, metadata },
});
document.dispatchEvent(e);
const e = new CustomEvent('definedInteraction', {
detail: { eventName, productId, metadata },
});
document.dispatchEvent(e);
};
interface Hotel {
id: string;
name: string;
roomType: string;
checkIn: string;
checkOut: string;
amenities: string[];
refundable: boolean;
pricePerNight: number;
nights: number;
id: string;
name: string;
roomType: string;
checkIn: string;
checkOut: string;
amenities: string[];
refundable: boolean;
pricePerNight: number;
nights: number;
}
const PriceDisplay = ({ price, perNight }: { price: number; perNight: boolean }) => (
<div className="price-wrapper">
<div className="price-label">{perNight ? 'Per night' : 'Total'}</div>
<div className="price-amount">${price}</div>
{perNight && <div className="price-unit">/night</div>}
</div>
<div className="price-wrapper">
<div className="price-label">{perNight ? 'Per night' : 'Total'}</div>
<div className="price-amount">${price}</div>
{perNight && <div className="price-unit">/night</div>}
</div>
);
const AmenityIcon = ({ name }: { name: string }) => {
const iconMap: Record<string, string> = {
wifi: 'Wi-Fi',
pool: 'Pool',
gym: 'Gym',
parking: 'Parking',
breakfast: 'Breakfast',
spa: 'Spa',
};
return <span className="feature-tag">{iconMap[name.toLowerCase()] || name}</span>;
const iconMap: Record<string, string> = {
wifi: 'Wi-Fi',
pool: 'Pool',
gym: 'Gym',
parking: 'Parking',
breakfast: 'Breakfast',
spa: 'Spa',
};
return <span className="feature-tag">{iconMap[name.toLowerCase()] || name}</span>;
};
export default function HotelCard({ hotel }: { hotel: Hotel }) {
const handleCardClick = () => {
dispatchInteraction('product_view', hotel.id, {
roomType: hotel.roomType,
price: hotel.pricePerNight,
nights: hotel.nights,
const titleRef = useHoverTracking({
eventName: 'hover_over_title',
productId: hotel.id,
metadata: { elementText: hotel.name },
});
};
return (
<div
className="hotel-card cursor-pointer"
onClick={handleCardClick}
onMouseEnter={() => dispatchInteraction('product_hover', hotel.id)}
>
<div className="hotel-image bg-gray-200 flex items-center justify-center">
<span className="text-gray-400 text-sm">Image</span>
</div>
const priceRef = useHoverTracking({
eventName: 'hover_over_paragraph',
productId: hotel.id,
metadata: { elementText: `$${hotel.pricePerNight}` },
});
<div className="hotel-info">
<h3 className="hotel-name">{hotel.name}</h3>
<div className="hotel-location text-sm mb-2">{hotel.roomType}</div>
<div className="text-sm text-[var(--text-secondary)] mb-2">
{hotel.checkIn} - {hotel.checkOut}
</div>
<div className="hotel-features">
{hotel.amenities.map((a) => (
<AmenityIcon key={a} name={a} />
))}
</div>
{hotel.refundable && (
<div className="free-cancellation mt-2">Free cancellation</div>
)}
</div>
const handleCardClick = () => {
dispatchInteraction('view_item_page', hotel.id, {
roomType: hotel.roomType,
price: hotel.pricePerNight,
nights: hotel.nights,
});
};
<div className="hotel-pricing">
<PriceDisplay price={hotel.pricePerNight} perNight />
<div className="text-xs text-[var(--text-secondary)] mt-1">
${hotel.pricePerNight * hotel.nights} total for {hotel.nights} night{hotel.nights > 1 ? 's' : ''}
return (
<div
className="hotel-card cursor-pointer"
onClick={handleCardClick}
>
<div className="hotel-image bg-gray-200 flex items-center justify-center">
<span className="text-gray-400 text-sm">Image</span>
</div>
<div className="hotel-info">
<h3 ref={titleRef} className="hotel-name">{hotel.name}</h3>
<div className="hotel-location text-sm mb-2">{hotel.roomType}</div>
<div className="text-sm text-[var(--text-secondary)] mb-2">
{hotel.checkIn} - {hotel.checkOut}
</div>
<div className="hotel-features">
{hotel.amenities.map((a) => (
<AmenityIcon key={a} name={a} />
))}
</div>
{hotel.refundable && (
<div className="free-cancellation mt-2">Free cancellation</div>
)}
</div>
<div className="hotel-pricing">
<div ref={priceRef}>
<PriceDisplay price={hotel.pricePerNight} perNight />
</div>
<div className="text-xs text-[var(--text-secondary)] mt-1">
${hotel.pricePerNight * hotel.nights} total for {hotel.nights} night{hotel.nights > 1 ? 's' : ''}
</div>
</div>
</div>
</div>
</div>
);
);
}

View File

@@ -42,23 +42,6 @@ export const useInteractionTracking = () => {
setReady(true);
});
const handleClick = (e: MouseEvent) => {
if (!sidRef.current) return;
const tgt = e.target as HTMLElement;
const page = window.location.pathname;
track({
sessionId: sidRef.current,
eventName: 'click',
page,
metadata: {
x: e.clientX,
y: e.clientY,
targetEl: tgt.tagName,
targetUrl: tgt instanceof HTMLAnchorElement ? tgt.href : undefined,
},
});
};
const handlePageView = () => {
if (!sidRef.current) return;
const page = window.location.pathname;
@@ -94,11 +77,9 @@ export const useInteractionTracking = () => {
if (!ready) return;
handlePageView();
document.addEventListener('click', handleClick);
document.addEventListener('definedInteraction', handleDefinedInteraction);
return () => {
document.removeEventListener('click', handleClick);
document.removeEventListener('definedInteraction', handleDefinedInteraction);
};
}, [ready]);