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'; 'use client';
import type { EventName } from '@/lib/events'; import type { EventName } from '@/lib/events';
import { useHoverTracking } from '@/hooks/useHoverTracking';
const dispatchInteraction = (eventName: EventName, productId?: string, metadata?: Record<string, unknown>) => { const dispatchInteraction = (eventName: EventName, productId?: string, metadata?: Record<string, unknown>) => {
const e = new CustomEvent('definedInteraction', { const e = new CustomEvent('definedInteraction', {
detail: { eventName, productId, metadata }, detail: { eventName, productId, metadata },
}); });
document.dispatchEvent(e); document.dispatchEvent(e);
}; };
type CabinClass = 'economy' | 'premium' | 'business' | 'first'; type CabinClass = 'economy' | 'premium' | 'business' | 'first';
type FareRule = 'flexible' | 'standard' | 'basic'; type FareRule = 'flexible' | 'standard' | 'basic';
interface Flight { interface Flight {
id: string; id: string;
departure: { time: string; airport: string }; departure: { time: string; airport: string };
arrival: { time: string; airport: string }; arrival: { time: string; airport: string };
duration: string; duration: string;
stops: number; stops: number;
cabinClass: CabinClass; cabinClass: CabinClass;
fareRule: FareRule; fareRule: FareRule;
refundable: boolean; refundable: boolean;
basePrice: number; basePrice: number;
} }
const PriceDisplay = ({ price }: { price: 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 }) { export default function AirlineCard({ flight }: { flight: Flight }) {
const handleCardClick = () => { const durationRef = useHoverTracking({
dispatchInteraction('product_view', flight.id, { eventName: 'hover_over_title',
cabinClass: flight.cabinClass, productId: flight.id,
fareRule: flight.fareRule, metadata: { elementText: flight.duration },
price: flight.basePrice,
}); });
};
return ( const priceRef = useHoverTracking({
<div eventName: 'hover_over_paragraph',
className="flight-card cursor-pointer" productId: flight.id,
onClick={handleCardClick} metadata: { elementText: `$${flight.basePrice}` },
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>
<div className="flight-route"> const handleCardClick = () => {
<div className="flight-duration">{flight.duration}</div> dispatchInteraction('view_item_page', flight.id, {
<div className="flight-stops"> cabinClass: flight.cabinClass,
{flight.stops === 0 ? 'Direct' : `${flight.stops} stop${flight.stops > 1 ? 's' : ''}`} 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> );
<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'; 'use client';
import type { EventName } from '@/lib/events'; import type { EventName } from '@/lib/events';
import { useHoverTracking } from '@/hooks/useHoverTracking';
const dispatchInteraction = (eventName: EventName, productId?: string, metadata?: Record<string, unknown>) => { const dispatchInteraction = (eventName: EventName, productId?: string, metadata?: Record<string, unknown>) => {
const e = new CustomEvent('definedInteraction', { const e = new CustomEvent('definedInteraction', {
detail: { eventName, productId, metadata }, detail: { eventName, productId, metadata },
}); });
document.dispatchEvent(e); document.dispatchEvent(e);
}; };
interface Hotel { interface Hotel {
id: string; id: string;
name: string; name: string;
roomType: string; roomType: string;
checkIn: string; checkIn: string;
checkOut: string; checkOut: string;
amenities: string[]; amenities: string[];
refundable: boolean; refundable: boolean;
pricePerNight: number; pricePerNight: number;
nights: number; nights: number;
} }
const PriceDisplay = ({ price, perNight }: { price: number; perNight: boolean }) => ( const PriceDisplay = ({ price, perNight }: { price: number; perNight: boolean }) => (
<div className="price-wrapper"> <div className="price-wrapper">
<div className="price-label">{perNight ? 'Per night' : 'Total'}</div> <div className="price-label">{perNight ? 'Per night' : 'Total'}</div>
<div className="price-amount">${price}</div> <div className="price-amount">${price}</div>
{perNight && <div className="price-unit">/night</div>} {perNight && <div className="price-unit">/night</div>}
</div> </div>
); );
const AmenityIcon = ({ name }: { name: string }) => { const AmenityIcon = ({ name }: { name: string }) => {
const iconMap: Record<string, string> = { const iconMap: Record<string, string> = {
wifi: 'Wi-Fi', wifi: 'Wi-Fi',
pool: 'Pool', pool: 'Pool',
gym: 'Gym', gym: 'Gym',
parking: 'Parking', parking: 'Parking',
breakfast: 'Breakfast', breakfast: 'Breakfast',
spa: 'Spa', spa: 'Spa',
}; };
return <span className="feature-tag">{iconMap[name.toLowerCase()] || name}</span>; return <span className="feature-tag">{iconMap[name.toLowerCase()] || name}</span>;
}; };
export default function HotelCard({ hotel }: { hotel: Hotel }) { export default function HotelCard({ hotel }: { hotel: Hotel }) {
const handleCardClick = () => { const titleRef = useHoverTracking({
dispatchInteraction('product_view', hotel.id, { eventName: 'hover_over_title',
roomType: hotel.roomType, productId: hotel.id,
price: hotel.pricePerNight, metadata: { elementText: hotel.name },
nights: hotel.nights,
}); });
};
return ( const priceRef = useHoverTracking({
<div eventName: 'hover_over_paragraph',
className="hotel-card cursor-pointer" productId: hotel.id,
onClick={handleCardClick} metadata: { elementText: `$${hotel.pricePerNight}` },
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>
<div className="hotel-info"> const handleCardClick = () => {
<h3 className="hotel-name">{hotel.name}</h3> dispatchInteraction('view_item_page', hotel.id, {
<div className="hotel-location text-sm mb-2">{hotel.roomType}</div> roomType: hotel.roomType,
<div className="text-sm text-[var(--text-secondary)] mb-2"> price: hotel.pricePerNight,
{hotel.checkIn} - {hotel.checkOut} nights: hotel.nights,
</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"> return (
<PriceDisplay price={hotel.pricePerNight} perNight /> <div
<div className="text-xs text-[var(--text-secondary)] mt-1"> className="hotel-card cursor-pointer"
${hotel.pricePerNight * hotel.nights} total for {hotel.nights} night{hotel.nights > 1 ? 's' : ''} 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> );
</div>
);
} }

View File

@@ -42,23 +42,6 @@ export const useInteractionTracking = () => {
setReady(true); 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 = () => { const handlePageView = () => {
if (!sidRef.current) return; if (!sidRef.current) return;
const page = window.location.pathname; const page = window.location.pathname;
@@ -94,11 +77,9 @@ export const useInteractionTracking = () => {
if (!ready) return; if (!ready) return;
handlePageView(); handlePageView();
document.addEventListener('click', handleClick);
document.addEventListener('definedInteraction', handleDefinedInteraction); document.addEventListener('definedInteraction', handleDefinedInteraction);
return () => { return () => {
document.removeEventListener('click', handleClick);
document.removeEventListener('definedInteraction', handleDefinedInteraction); document.removeEventListener('definedInteraction', handleDefinedInteraction);
}; };
}, [ready]); }, [ready]);