@times-components/ts-components 1.146.2-be27d508c972211ad80599875cd69c63bf67d4b1.45 → 1.146.2-c12ed7999a41984c2ba8c437357e7a5df1914881.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,8 @@ import {
26
26
  StyledLink
27
27
  } from './styles';
28
28
  import { MoonIcon, LocationIcon, BoatIcon, CalendarIcon } from './assets';
29
+ import { tealiumTrackingHandler } from '../../helpers/tracking/TrackingHandler';
30
+ import { getPreferredEdition } from '../../utils/cookie';
29
31
 
30
32
  export const TripCard: FC<TripCardProps> = ({
31
33
  card,
@@ -33,10 +35,37 @@ export const TripCard: FC<TripCardProps> = ({
33
35
  imgHeight = {}
34
36
  }) => {
35
37
  const [imageLoaded, setImageLoaded] = useState(false);
38
+ const trackEvent = (eventName: string) => {
39
+ tealiumTrackingHandler(
40
+ eventName,
41
+ 'navigation',
42
+ 'click',
43
+ card.headline,
44
+ undefined,
45
+ {
46
+ app_content_location: getPreferredEdition()
47
+ }
48
+ );
49
+ };
50
+
51
+ const handleCardClick = (type: 'headline' | 'img') => () =>
52
+ type === 'headline'
53
+ ? trackEvent(`trip card: ${card.headline}`)
54
+ : trackEvent('trip card: img click');
55
+
56
+ const handleCTAClick = () =>
57
+ trackEvent(`trip card: ${card.cta_text || 'View Itinerary'}`);
58
+
59
+ const handleLogoClick = () => trackEvent(`trip card: ${card.logo_name}`);
36
60
 
37
61
  return (
38
62
  <CardContainer>
39
- <StyledLink href={card.cta_url} target="_blank" rel="noopener noreferrer">
63
+ <StyledLink
64
+ href={card.cta_url}
65
+ target="_blank"
66
+ rel="noopener noreferrer"
67
+ onClick={handleCardClick('img')}
68
+ >
40
69
  <ImageContainer isStaticGrid={isStaticGrid} {...imgHeight}>
41
70
  {!imageLoaded && <Placeholder />}
42
71
  <CardImage
@@ -56,6 +85,7 @@ export const TripCard: FC<TripCardProps> = ({
56
85
  href={card.cta_url}
57
86
  target="_blank"
58
87
  rel="noopener noreferrer"
88
+ onClick={handleCardClick('headline')}
59
89
  >
60
90
  <Headline>{card.headline}</Headline>
61
91
  </StyledLink>
@@ -119,6 +149,7 @@ export const TripCard: FC<TripCardProps> = ({
119
149
  href={card.logo_url}
120
150
  target="_blank"
121
151
  rel="noopener noreferrer"
152
+ onClick={handleLogoClick}
122
153
  >
123
154
  <img src={card.logo} alt="Partner logo" />
124
155
  </a>
@@ -130,6 +161,7 @@ export const TripCard: FC<TripCardProps> = ({
130
161
  href={card.cta_url}
131
162
  target="_blank"
132
163
  rel="noopener noreferrer"
164
+ onClick={handleCTAClick}
133
165
  >
134
166
  {card.cta_text || 'View Itinerary'}
135
167
  </CTAButton>
@@ -13,6 +13,8 @@ import {
13
13
  StyledLink
14
14
  } from './styles';
15
15
  import { ChevronRightIcon } from './assets';
16
+ import { tealiumTrackingHandler } from '../../helpers/tracking/TrackingHandler';
17
+ import { getPreferredEdition } from '../../utils/cookie';
16
18
 
17
19
  export const TripCardsLayout: FC<TripCardsLayoutProps> = ({
18
20
  element,
@@ -75,6 +77,19 @@ export const TripCardsLayout: FC<TripCardsLayoutProps> = ({
75
77
  const shouldUseCarousel =
76
78
  (isTabletMobile && items.length >= 3) || !isStaticGrid || hasOverflow;
77
79
 
80
+ const onTitleClick = () => {
81
+ tealiumTrackingHandler(
82
+ `trip cards component ${title}`,
83
+ 'navigation',
84
+ 'click',
85
+ undefined,
86
+ undefined,
87
+ {
88
+ app_content_location: getPreferredEdition()
89
+ }
90
+ );
91
+ };
92
+
78
93
  return (
79
94
  <Container data-testid="trip-cards-container" {...widthContainerConfig}>
80
95
  <TitleSection data-testid="title-section">
@@ -88,6 +103,7 @@ export const TripCardsLayout: FC<TripCardsLayoutProps> = ({
88
103
  href={titleurl}
89
104
  target="_blank"
90
105
  rel="noopener noreferrer"
106
+ onClick={onTitleClick}
91
107
  data-testid="trip-cards-title-link"
92
108
  >
93
109
  <Title data-testid="trip-cards-title">{title}</Title>
@@ -103,6 +119,7 @@ export const TripCardsLayout: FC<TripCardsLayoutProps> = ({
103
119
  target="_blank"
104
120
  rel="noopener noreferrer"
105
121
  data-testid="title-link"
122
+ onClick={onTitleClick}
106
123
  >
107
124
  <ChevronRightIcon />
108
125
  </TitleLink>
@@ -3,6 +3,11 @@ import { render, screen, fireEvent } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
4
  import { TripCard } from '../TripCard';
5
5
  import { TripCardApiData } from '../types';
6
+ import { tealiumTrackingHandler } from '../../../helpers/tracking/TrackingHandler';
7
+
8
+ jest.mock('../../../helpers/tracking/TrackingHandler', () => ({
9
+ tealiumTrackingHandler: jest.fn()
10
+ }));
6
11
 
7
12
  const mockCard: TripCardApiData = {
8
13
  cruise_id: '2074350',
@@ -15,11 +20,16 @@ const mockCard: TripCardApiData = {
15
20
  price: '£2000',
16
21
  logo: 'https://example.com/logo.png',
17
22
  logo_url: 'https://example.com',
23
+ logo_name: 'MSC Cruises',
18
24
  cta_url: 'https://example.com/cruise',
19
25
  cta_text: 'View Itinerary'
20
26
  };
21
27
 
22
28
  describe('TripCard', () => {
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ });
32
+
23
33
  it('renders card with all required data', () => {
24
34
  render(<TripCard card={mockCard} />);
25
35
 
@@ -117,4 +127,129 @@ describe('TripCard', () => {
117
127
  fireEvent.load(cardImage);
118
128
  expect(cardImage).toHaveStyle({ display: 'block' });
119
129
  });
130
+
131
+ describe('Tracking', () => {
132
+ it('calls tealiumTrackingHandler when headline is clicked', () => {
133
+ render(<TripCard card={mockCard} />);
134
+
135
+ const headlineLink = screen.getAllByRole('link')[1];
136
+ fireEvent.click(headlineLink);
137
+
138
+ expect(tealiumTrackingHandler).toHaveBeenCalledWith(
139
+ 'trip card: Mediterranean from Barcelona',
140
+ 'navigation',
141
+ 'click',
142
+ 'Mediterranean from Barcelona',
143
+ undefined,
144
+ {
145
+ app_content_location: expect.any(String)
146
+ }
147
+ );
148
+ });
149
+
150
+ it('calls tealiumTrackingHandler when image is clicked', () => {
151
+ render(<TripCard card={mockCard} />);
152
+
153
+ const imageLink = screen.getAllByRole('link')[0];
154
+ fireEvent.click(imageLink);
155
+
156
+ expect(tealiumTrackingHandler).toHaveBeenCalledWith(
157
+ 'trip card: img click',
158
+ 'navigation',
159
+ 'click',
160
+ 'Mediterranean from Barcelona',
161
+ undefined,
162
+ {
163
+ app_content_location: expect.any(String)
164
+ }
165
+ );
166
+ });
167
+
168
+ it('calls tealiumTrackingHandler when CTA button is clicked', () => {
169
+ render(<TripCard card={mockCard} />);
170
+
171
+ const ctaButton = screen.getByText('View Itinerary');
172
+ fireEvent.click(ctaButton);
173
+
174
+ expect(tealiumTrackingHandler).toHaveBeenCalledWith(
175
+ 'trip card: View Itinerary',
176
+ 'navigation',
177
+ 'click',
178
+ 'Mediterranean from Barcelona',
179
+ undefined,
180
+ {
181
+ app_content_location: expect.any(String)
182
+ }
183
+ );
184
+ });
185
+
186
+ it('calls tealiumTrackingHandler with default CTA text when not provided', () => {
187
+ const cardWithoutCTAText = {
188
+ ...mockCard,
189
+ cta_text: undefined
190
+ };
191
+
192
+ render(<TripCard card={cardWithoutCTAText} />);
193
+
194
+ const ctaButton = screen.getByText('View Itinerary');
195
+ fireEvent.click(ctaButton);
196
+
197
+ expect(tealiumTrackingHandler).toHaveBeenCalledWith(
198
+ 'trip card: View Itinerary',
199
+ 'navigation',
200
+ 'click',
201
+ 'Mediterranean from Barcelona',
202
+ undefined,
203
+ {
204
+ app_content_location: expect.any(String)
205
+ }
206
+ );
207
+ });
208
+
209
+ it('calls tealiumTrackingHandler when logo is clicked with empty logo_name', () => {
210
+ const cardWithoutLogoName = {
211
+ ...mockCard,
212
+ logo_name: ''
213
+ };
214
+
215
+ render(<TripCard card={cardWithoutLogoName} />);
216
+
217
+ const logoLink = screen.getByRole('link', { name: /partner logo/i });
218
+ fireEvent.click(logoLink);
219
+
220
+ expect(tealiumTrackingHandler).toHaveBeenCalledWith(
221
+ 'trip card: ',
222
+ 'navigation',
223
+ 'click',
224
+ 'Mediterranean from Barcelona',
225
+ undefined,
226
+ {
227
+ app_content_location: expect.any(String)
228
+ }
229
+ );
230
+ });
231
+
232
+ it('calls tealiumTrackingHandler with logo_name when provided', () => {
233
+ const cardWithLogoName = {
234
+ ...mockCard,
235
+ logo_name: 'MSC Cruises'
236
+ };
237
+
238
+ render(<TripCard card={cardWithLogoName} />);
239
+
240
+ const logoLink = screen.getByRole('link', { name: /partner logo/i });
241
+ fireEvent.click(logoLink);
242
+
243
+ expect(tealiumTrackingHandler).toHaveBeenCalledWith(
244
+ 'trip card: MSC Cruises',
245
+ 'navigation',
246
+ 'click',
247
+ 'Mediterranean from Barcelona',
248
+ undefined,
249
+ {
250
+ app_content_location: expect.any(String)
251
+ }
252
+ );
253
+ });
254
+ });
120
255
  });
@@ -1,8 +1,13 @@
1
1
  import React from 'react';
2
- import { render, screen } from '@testing-library/react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
4
  import { TripCardsLayout } from '../TripCardsLayout';
5
5
  import { TripCardApiData } from '../types';
6
+ import { tealiumTrackingHandler } from '../../../helpers/tracking/TrackingHandler';
7
+
8
+ jest.mock('../../../helpers/tracking/TrackingHandler', () => ({
9
+ tealiumTrackingHandler: jest.fn()
10
+ }));
6
11
 
7
12
  const mockElement = {
8
13
  class: 'trip-cards',
@@ -33,6 +38,7 @@ global.ResizeObserver = class ResizeObserver {
33
38
 
34
39
  describe('TripCardsLayout', () => {
35
40
  beforeEach(() => {
41
+ jest.clearAllMocks();
36
42
  Element.prototype.scrollTo = jest.fn();
37
43
 
38
44
  // Mock matchMedia for each test
@@ -52,6 +58,10 @@ describe('TripCardsLayout', () => {
52
58
  });
53
59
  });
54
60
 
61
+ afterEach(() => {
62
+ jest.clearAllMocks();
63
+ });
64
+
55
65
  describe('Rendering', () => {
56
66
  it('renders with title and description', () => {
57
67
  const items = [
@@ -245,6 +255,7 @@ describe('TripCardsLayout', () => {
245
255
  price: '£1000',
246
256
  logo: 'logo.png',
247
257
  logo_url: 'https://example.com',
258
+ logo_name: 'Test Cruise Line',
248
259
  cta_url: 'https://example.com',
249
260
  cta_text: 'View'
250
261
  };
@@ -400,6 +411,7 @@ describe('TripCardsLayout', () => {
400
411
  price: '£2000',
401
412
  logo: 'logo.png',
402
413
  logo_url: 'https://example.com',
414
+ logo_name: 'Test Cruise Line',
403
415
  cta_url: 'https://example.com',
404
416
  cta_text: 'Book'
405
417
  };
@@ -580,4 +592,80 @@ describe('TripCardsLayout', () => {
580
592
  }
581
593
  });
582
594
  });
595
+
596
+ describe('Tracking', () => {
597
+ it('calls tealiumTrackingHandler when title link is clicked', () => {
598
+ const items = [{ id: '1', data: undefined }];
599
+
600
+ render(
601
+ <TripCardsLayout
602
+ element={mockElement}
603
+ items={items}
604
+ CardComponent={MockCard}
605
+ itemsPerPage={2}
606
+ />
607
+ );
608
+
609
+ const titleLink = screen.getByTestId('trip-cards-title-link');
610
+ fireEvent.click(titleLink);
611
+
612
+ expect(tealiumTrackingHandler).toHaveBeenCalledWith(
613
+ 'trip cards component Test Cruises',
614
+ 'navigation',
615
+ 'click',
616
+ undefined,
617
+ undefined,
618
+ {
619
+ app_content_location: 'uk'
620
+ }
621
+ );
622
+ });
623
+
624
+ it('calls tealiumTrackingHandler when icon link is clicked', () => {
625
+ const items = [{ id: '1', data: undefined }];
626
+
627
+ render(
628
+ <TripCardsLayout
629
+ element={mockElement}
630
+ items={items}
631
+ CardComponent={MockCard}
632
+ itemsPerPage={2}
633
+ />
634
+ );
635
+
636
+ const iconLink = screen.getByTestId('title-link');
637
+ fireEvent.click(iconLink);
638
+
639
+ expect(tealiumTrackingHandler).toHaveBeenCalledWith(
640
+ 'trip cards component Test Cruises',
641
+ 'navigation',
642
+ 'click',
643
+ undefined,
644
+ undefined,
645
+ {
646
+ app_content_location: 'uk'
647
+ }
648
+ );
649
+ });
650
+
651
+ it('does not call tealiumTrackingHandler when title link is not clicked', () => {
652
+ const elementWithoutLink = {
653
+ ...mockElement,
654
+ titleurl: undefined
655
+ };
656
+
657
+ const items = [{ id: '1', data: undefined }];
658
+
659
+ render(
660
+ <TripCardsLayout
661
+ element={elementWithoutLink}
662
+ items={items}
663
+ CardComponent={MockCard}
664
+ itemsPerPage={2}
665
+ />
666
+ );
667
+
668
+ expect(tealiumTrackingHandler).not.toHaveBeenCalled();
669
+ });
670
+ });
583
671
  });
@@ -61,6 +61,7 @@ describe('transformApiResult', () => {
61
61
  price: '£2000',
62
62
  logo: 'https://example.com/msc-logo.png',
63
63
  logo_url: 'https://example.com/cruise-lines/msc-cruises/',
64
+ logo_name: 'MSC Cruises',
64
65
  cta_url: 'https://example.com/cruises/mediterranean-from-barcelona',
65
66
  cta_text: 'View Itinerary'
66
67
  });
@@ -34,6 +34,7 @@ describe('TripCards', () => {
34
34
  original_price: undefined,
35
35
  logo: 'logo1.png',
36
36
  logo_url: 'https://example.com',
37
+ logo_name: 'Mock Line 1',
37
38
  cta_url: 'link1',
38
39
  cta_text: 'View'
39
40
  },
@@ -49,6 +50,7 @@ describe('TripCards', () => {
49
50
  original_price: undefined,
50
51
  logo: 'logo2.png',
51
52
  logo_url: 'https://example.com',
53
+ logo_name: 'Mock Line 2',
52
54
  cta_url: 'link2',
53
55
  cta_text: 'View'
54
56
  },
@@ -64,6 +66,7 @@ describe('TripCards', () => {
64
66
  original_price: undefined,
65
67
  logo: 'logo3.png',
66
68
  logo_url: 'https://example.com',
69
+ logo_name: 'Mock Line 3',
67
70
  cta_url: 'link3',
68
71
  cta_text: 'View'
69
72
  }
@@ -429,6 +432,7 @@ describe('TripCards', () => {
429
432
  original_price: undefined,
430
433
  logo: 'logo3.png',
431
434
  logo_url: 'https://example.com',
435
+ logo_name: 'Mock Line 3',
432
436
  cta_url: 'link3',
433
437
  cta_text: 'View'
434
438
  }
@@ -87,6 +87,7 @@ export const transformApiResult = (
87
87
  ),
88
88
  logo: (result.cruise_line && result.cruise_line.logo) || '',
89
89
  logo_url: (result.cruise_line && result.cruise_line.link) || '',
90
+ logo_name: (result.cruise_line && result.cruise_line.name) || '',
90
91
  cta_url: result.link || '',
91
92
  cta_text: 'View Itinerary'
92
93
  };
@@ -139,19 +140,23 @@ const isValidOffer = (result: ApiCruiseResult): boolean => {
139
140
  };
140
141
 
141
142
  export const fetchCruiseCards = async (
142
- cruiseIds: number[]
143
+ cruiseIds: number[],
144
+ hostName?: string
143
145
  ): Promise<TripCardApiData[]> => {
144
146
  const formData = new FormData();
145
147
  formData.append('action', 'results');
146
148
  formData.append('cruise_ids', cruiseIds.join(','));
147
-
148
- const response = await fetch(
149
- 'https://www.staging-thetimes.com/holidays/wp-admin/admin-ajax.php',
150
- {
151
- method: 'POST',
152
- body: formData
153
- }
154
- );
149
+ const url =
150
+ hostName &&
151
+ hostName.includes('thetimes.com') &&
152
+ !hostName.includes('-thetimes')
153
+ ? 'https://www.thetimes.com/holidays/wp-admin/admin-ajax.php'
154
+ : 'https://www.staging-thetimes.com/holidays/wp-admin/admin-ajax.php';
155
+
156
+ const response = await fetch(url, {
157
+ method: 'POST',
158
+ body: formData
159
+ });
155
160
 
156
161
  if (!response.ok) {
157
162
  throw new Error(`HTTP error! status: ${response.status}`);
@@ -13,7 +13,8 @@ export const TripCards: FC<TripCardsProps> = ({
13
13
  widthItemConfig,
14
14
  maxWidthItemConfig,
15
15
  imgHeight,
16
- forceStaticGrid
16
+ forceStaticGrid,
17
+ hostName
17
18
  }) => {
18
19
  const [cards, setCards] = useState<TripCardApiData[]>([]);
19
20
  const [loading, setLoading] = useState(true);
@@ -39,7 +40,7 @@ export const TripCards: FC<TripCardsProps> = ({
39
40
  }
40
41
 
41
42
  try {
42
- const transformedCards = await fetchCruiseCards(allIds);
43
+ const transformedCards = await fetchCruiseCards(allIds, hostName);
43
44
  setCards(transformedCards);
44
45
  } catch (e) {
45
46
  setError(e instanceof Error ? e.message : 'Failed to load cruises');
@@ -35,6 +35,7 @@ export interface TripCardsProps {
35
35
  maxWidthItemConfig?: ResponsiveConfig;
36
36
  imgHeight?: ImageHeightConfig;
37
37
  forceStaticGrid?: boolean;
38
+ hostName?: string;
38
39
  }
39
40
 
40
41
  export interface DecodedTripCard {
@@ -55,8 +56,9 @@ export interface TripCardApiData {
55
56
  price: string;
56
57
  logo: string;
57
58
  logo_url: string;
59
+ logo_name: string;
58
60
  cta_url: string;
59
- cta_text: string;
61
+ cta_text?: string;
60
62
  }
61
63
 
62
64
  export interface TripCardProps {
@@ -1,3 +1,11 @@
1
1
  export const hasCookieConsent = () =>
2
2
  typeof window !== 'undefined' &&
3
3
  window.document.cookie.indexOf('nuk-consent-personalisation=1') >= 0;
4
+
5
+ export const getPreferredEdition = () => {
6
+ if (!window || !window.nuk) {
7
+ return 'uk';
8
+ }
9
+ const edition = window.nuk.getCookieValue('nuk_preferred_edition') || 'uk';
10
+ return edition.toLowerCase();
11
+ };