mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
refactor tracking to ues callbacks instead of refs
This commit is contained in:
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
Reference in New Issue
Block a user