@times-components/ts-components 1.145.1-76ee0965069e2a17bc1f8dcf02d24e8fefd6869a.0 → 1.145.1-7e7a12feaf05c514789e802bf49cadca92e6a2b9.10

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.
Files changed (143) hide show
  1. package/dist/components/carousel-component/CarouselComponent.stories.js +146 -0
  2. package/dist/components/carousel-component/CarouselItem.d.ts +3 -0
  3. package/dist/components/carousel-component/CarouselItem.js +11 -0
  4. package/dist/components/carousel-component/DefaultNavigationArrow.d.ts +8 -0
  5. package/dist/components/carousel-component/DefaultNavigationArrow.js +6 -0
  6. package/dist/components/carousel-component/DefaultPageDot.d.ts +8 -0
  7. package/dist/components/carousel-component/DefaultPageDot.js +4 -0
  8. package/dist/components/{opta/football/opta-match-stats/matchday-live/__tests__/OptaMatchStatsMatchdayLive.test.d.ts → carousel-component/__tests__/CarouselComponent.test.d.ts} +0 -1
  9. package/dist/components/carousel-component/__tests__/CarouselComponent.test.js +163 -0
  10. package/dist/components/carousel-component/__tests__/CarouselItem.test.d.ts +1 -0
  11. package/dist/components/carousel-component/__tests__/CarouselItem.test.js +80 -0
  12. package/dist/components/carousel-component/__tests__/DefaultNavigationArrow.test.d.ts +1 -0
  13. package/dist/components/carousel-component/__tests__/DefaultNavigationArrow.test.js +62 -0
  14. package/dist/components/carousel-component/__tests__/DefaultPageDot.test.d.ts +1 -0
  15. package/dist/components/carousel-component/__tests__/DefaultPageDot.test.js +68 -0
  16. package/dist/components/carousel-component/hooks/__tests__/useCarousel.test.d.ts +1 -0
  17. package/dist/components/carousel-component/hooks/__tests__/useCarousel.test.js +459 -0
  18. package/dist/components/carousel-component/hooks/useCarousel.d.ts +2 -0
  19. package/dist/components/carousel-component/hooks/useCarousel.js +175 -0
  20. package/dist/components/carousel-component/index.d.ts +4 -0
  21. package/dist/components/carousel-component/index.js +20 -0
  22. package/dist/components/carousel-component/styles.d.ts +27 -0
  23. package/dist/components/carousel-component/styles.js +169 -0
  24. package/dist/components/carousel-component/types.d.ts +53 -0
  25. package/dist/components/carousel-component/types.js +2 -0
  26. package/dist/components/opta/football/opta-match-stats/shared/styles.js +1 -8
  27. package/dist/components/trip-cards/SkeletonCard.d.ts +6 -0
  28. package/dist/components/trip-cards/SkeletonCard.js +21 -0
  29. package/dist/components/trip-cards/TripCard.d.ts +3 -0
  30. package/dist/components/trip-cards/TripCard.js +49 -0
  31. package/dist/components/trip-cards/TripCards.stories.d.ts +1 -0
  32. package/dist/components/trip-cards/TripCards.stories.js +189 -0
  33. package/dist/components/trip-cards/TripCardsLayout.d.ts +3 -0
  34. package/dist/components/trip-cards/TripCardsLayout.js +37 -0
  35. package/dist/components/trip-cards/__tests__/SkeletonCard.test.d.ts +1 -0
  36. package/dist/components/trip-cards/__tests__/SkeletonCard.test.js +139 -0
  37. package/dist/components/trip-cards/__tests__/TripCard.test.d.ts +1 -0
  38. package/dist/components/trip-cards/__tests__/TripCard.test.js +95 -0
  39. package/dist/components/trip-cards/__tests__/TripCardsLayout.test.d.ts +1 -0
  40. package/dist/components/trip-cards/__tests__/TripCardsLayout.test.js +277 -0
  41. package/dist/components/trip-cards/__tests__/assets.test.d.ts +1 -0
  42. package/dist/components/trip-cards/__tests__/assets.test.js +165 -0
  43. package/dist/components/trip-cards/__tests__/helpers.test.d.ts +1 -0
  44. package/dist/components/trip-cards/__tests__/helpers.test.js +216 -0
  45. package/dist/components/trip-cards/__tests__/index.test.d.ts +1 -0
  46. package/dist/components/trip-cards/__tests__/index.test.js +433 -0
  47. package/dist/components/trip-cards/__tests__/mockData.test.d.ts +1 -0
  48. package/dist/components/trip-cards/__tests__/mockData.test.js +57 -0
  49. package/dist/components/trip-cards/__tests__/skeletonStyles.test.d.ts +1 -0
  50. package/dist/components/trip-cards/__tests__/skeletonStyles.test.js +194 -0
  51. package/dist/components/trip-cards/assets/BoatIcon.d.ts +1 -0
  52. package/dist/components/trip-cards/assets/BoatIcon.js +4 -0
  53. package/dist/components/trip-cards/assets/CalendarIcon.d.ts +1 -0
  54. package/dist/components/trip-cards/assets/CalendarIcon.js +4 -0
  55. package/dist/components/trip-cards/assets/ChevronRightIcon.d.ts +1 -0
  56. package/dist/components/trip-cards/assets/ChevronRightIcon.js +4 -0
  57. package/dist/components/trip-cards/assets/LocationIcon.d.ts +1 -0
  58. package/dist/components/trip-cards/assets/LocationIcon.js +4 -0
  59. package/dist/components/trip-cards/assets/MoonIcon.d.ts +1 -0
  60. package/dist/components/trip-cards/assets/MoonIcon.js +4 -0
  61. package/dist/components/trip-cards/assets/index.d.ts +6 -0
  62. package/dist/components/trip-cards/assets/index.js +7 -0
  63. package/dist/components/trip-cards/helpers.d.ts +4 -0
  64. package/dist/components/trip-cards/helpers.js +115 -0
  65. package/dist/components/trip-cards/index.d.ts +4 -0
  66. package/dist/components/trip-cards/index.js +70 -0
  67. package/dist/components/trip-cards/mockData.d.ts +3 -0
  68. package/dist/components/trip-cards/mockData.js +317 -0
  69. package/dist/components/trip-cards/skeletonStyles.d.ts +9 -0
  70. package/dist/components/trip-cards/skeletonStyles.js +37 -0
  71. package/dist/components/trip-cards/styles.d.ts +38 -0
  72. package/dist/components/trip-cards/styles.js +401 -0
  73. package/dist/components/trip-cards/types.d.ts +119 -0
  74. package/dist/components/trip-cards/types.js +2 -0
  75. package/dist/index.d.ts +1 -0
  76. package/dist/index.js +2 -4
  77. package/package.json +3 -3
  78. package/rnw.js +1 -1
  79. package/src/components/carousel-component/CarouselComponent.stories.tsx +220 -0
  80. package/src/components/carousel-component/CarouselItem.tsx +25 -0
  81. package/src/components/carousel-component/DefaultNavigationArrow.tsx +37 -0
  82. package/src/components/carousel-component/DefaultPageDot.tsx +20 -0
  83. package/src/components/carousel-component/__tests__/CarouselComponent.test.tsx +259 -0
  84. package/src/components/carousel-component/__tests__/CarouselItem.test.tsx +140 -0
  85. package/src/components/carousel-component/__tests__/DefaultNavigationArrow.test.tsx +153 -0
  86. package/src/components/carousel-component/__tests__/DefaultPageDot.test.tsx +105 -0
  87. package/src/components/carousel-component/hooks/__tests__/useCarousel.test.ts +625 -0
  88. package/src/components/carousel-component/hooks/useCarousel.ts +231 -0
  89. package/src/components/carousel-component/index.tsx +92 -0
  90. package/src/components/carousel-component/styles.ts +185 -0
  91. package/src/components/carousel-component/types.ts +62 -0
  92. package/src/components/opta/football/opta-match-stats/commentary/__tests__/__snapshots__/OptaMatchStatsCommentary.test.tsx.snap +1 -1
  93. package/src/components/opta/football/opta-match-stats/shared/styles.ts +0 -8
  94. package/src/components/opta/football/opta-match-stats/stats-graphs/__tests__/__snapshots__/OptaMatchStatsGraphs.test.tsx.snap +1 -1
  95. package/src/components/trip-cards/SkeletonCard.tsx +62 -0
  96. package/src/components/trip-cards/TripCard.tsx +143 -0
  97. package/src/components/trip-cards/TripCards.stories.tsx +254 -0
  98. package/src/components/trip-cards/TripCardsLayout.tsx +108 -0
  99. package/src/components/trip-cards/__tests__/SkeletonCard.test.tsx +169 -0
  100. package/src/components/trip-cards/__tests__/TripCard.test.tsx +120 -0
  101. package/src/components/trip-cards/__tests__/TripCardsLayout.test.tsx +532 -0
  102. package/src/components/trip-cards/__tests__/assets.test.tsx +206 -0
  103. package/src/components/trip-cards/__tests__/helpers.test.ts +266 -0
  104. package/src/components/trip-cards/__tests__/index.test.tsx +495 -0
  105. package/src/components/trip-cards/__tests__/mockData.test.ts +67 -0
  106. package/src/components/trip-cards/__tests__/skeletonStyles.test.tsx +256 -0
  107. package/src/components/trip-cards/assets/BoatIcon.tsx +17 -0
  108. package/src/components/trip-cards/assets/CalendarIcon.tsx +17 -0
  109. package/src/components/trip-cards/assets/ChevronRightIcon.tsx +20 -0
  110. package/src/components/trip-cards/assets/LocationIcon.tsx +17 -0
  111. package/src/components/trip-cards/assets/MoonIcon.tsx +17 -0
  112. package/src/components/trip-cards/assets/index.ts +7 -0
  113. package/src/components/trip-cards/helpers.ts +150 -0
  114. package/src/components/trip-cards/index.tsx +119 -0
  115. package/src/components/trip-cards/mockData.ts +345 -0
  116. package/src/components/trip-cards/skeletonStyles.ts +46 -0
  117. package/src/components/trip-cards/styles.ts +446 -0
  118. package/src/components/trip-cards/types.ts +128 -0
  119. package/src/index.ts +2 -3
  120. package/dist/components/opta/football/opta-match-stats/matchday-live/DesktopWidget.d.ts +0 -10
  121. package/dist/components/opta/football/opta-match-stats/matchday-live/DesktopWidget.js +0 -69
  122. package/dist/components/opta/football/opta-match-stats/matchday-live/MobileWidget.d.ts +0 -12
  123. package/dist/components/opta/football/opta-match-stats/matchday-live/MobileWidget.js +0 -90
  124. package/dist/components/opta/football/opta-match-stats/matchday-live/OptaMatchStatsMatchdayLive.d.ts +0 -12
  125. package/dist/components/opta/football/opta-match-stats/matchday-live/OptaMatchStatsMatchdayLive.js +0 -10
  126. package/dist/components/opta/football/opta-match-stats/matchday-live/OptaMatchStatsMatchdayLive.stories.js +0 -24
  127. package/dist/components/opta/football/opta-match-stats/matchday-live/__tests__/OptaMatchStatsMatchdayLive.test.js +0 -48
  128. package/dist/components/opta/football/opta-match-stats/matchday-live/styles/MatchdayLiveController.d.ts +0 -1
  129. package/dist/components/opta/football/opta-match-stats/matchday-live/styles/MatchdayLiveController.js +0 -19
  130. package/dist/components/opta/football/opta-match-stats/matchday-live/styles/NavigationWrapper.d.ts +0 -12
  131. package/dist/components/opta/football/opta-match-stats/matchday-live/styles/NavigationWrapper.js +0 -67
  132. package/dist/components/opta/football/opta-match-stats/matchday-live/styles/WidgetContainer.d.ts +0 -6
  133. package/dist/components/opta/football/opta-match-stats/matchday-live/styles/WidgetContainer.js +0 -714
  134. package/src/components/opta/football/opta-match-stats/matchday-live/DesktopWidget.tsx +0 -108
  135. package/src/components/opta/football/opta-match-stats/matchday-live/MobileWidget.tsx +0 -158
  136. package/src/components/opta/football/opta-match-stats/matchday-live/OptaMatchStatsMatchdayLive.stories.tsx +0 -38
  137. package/src/components/opta/football/opta-match-stats/matchday-live/OptaMatchStatsMatchdayLive.tsx +0 -23
  138. package/src/components/opta/football/opta-match-stats/matchday-live/__tests__/OptaMatchStatsMatchdayLive.test.tsx +0 -61
  139. package/src/components/opta/football/opta-match-stats/matchday-live/__tests__/__snapshots__/OptaMatchStatsMatchdayLive.test.tsx.snap +0 -61
  140. package/src/components/opta/football/opta-match-stats/matchday-live/styles/MatchdayLiveController.tsx +0 -19
  141. package/src/components/opta/football/opta-match-stats/matchday-live/styles/NavigationWrapper.tsx +0 -81
  142. package/src/components/opta/football/opta-match-stats/matchday-live/styles/WidgetContainer.tsx +0 -745
  143. /package/dist/components/{opta/football/opta-match-stats/matchday-live/OptaMatchStatsMatchdayLive.stories.d.ts → carousel-component/CarouselComponent.stories.d.ts} +0 -0
@@ -0,0 +1,256 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import {
5
+ SkeletonLine,
6
+ SkeletonHeadline,
7
+ SkeletonPrice,
8
+ SkeletonButton
9
+ } from '../skeletonStyles';
10
+
11
+ describe('Skeleton Styles', () => {
12
+ describe('SkeletonLine', () => {
13
+ it('renders with default width', () => {
14
+ const { container } = render(<SkeletonLine />);
15
+ const element = container.firstChild;
16
+ expect(element).toBeInTheDocument();
17
+ });
18
+
19
+ it('accepts custom width prop', () => {
20
+ const { container } = render(<SkeletonLine width="80px" />);
21
+ const element = container.firstChild;
22
+ expect(element).toBeInTheDocument();
23
+ });
24
+
25
+ it('accepts custom marginBottom prop', () => {
26
+ const { container } = render(<SkeletonLine marginBottom="4px" />);
27
+ const element = container.firstChild;
28
+ expect(element).toBeInTheDocument();
29
+ });
30
+
31
+ it('accepts both width and marginBottom props', () => {
32
+ const { container } = render(
33
+ <SkeletonLine width="60px" marginBottom="8px" />
34
+ );
35
+ const element = container.firstChild;
36
+ expect(element).toBeInTheDocument();
37
+ });
38
+
39
+ it('renders as a div element', () => {
40
+ const { container } = render(<SkeletonLine />);
41
+ const element = container.firstChild;
42
+ if (element) {
43
+ expect(element.nodeName).toBe('DIV');
44
+ }
45
+ });
46
+
47
+ it('applies skeleton styling', () => {
48
+ const { container } = render(<SkeletonLine />);
49
+ const element = container.firstChild as HTMLElement;
50
+ expect(element.className).toMatch(/sc-/);
51
+ });
52
+ });
53
+
54
+ describe('SkeletonHeadline', () => {
55
+ it('renders correctly', () => {
56
+ const { container } = render(<SkeletonHeadline />);
57
+ const element = container.firstChild;
58
+ expect(element).toBeInTheDocument();
59
+ });
60
+
61
+ it('renders as a div element', () => {
62
+ const { container } = render(<SkeletonHeadline />);
63
+ const element = container.firstChild;
64
+ if (element) {
65
+ expect(element.nodeName).toBe('DIV');
66
+ }
67
+ });
68
+
69
+ it('has styled component class', () => {
70
+ const { container } = render(<SkeletonHeadline />);
71
+ const element = container.firstChild as HTMLElement;
72
+ expect(element.className).toMatch(/sc-/);
73
+ });
74
+
75
+ it('accepts className prop', () => {
76
+ const { container } = render(<SkeletonHeadline className="custom" />);
77
+ const element = container.firstChild as HTMLElement;
78
+ expect(element).toHaveClass('custom');
79
+ });
80
+ });
81
+
82
+ describe('SkeletonPrice', () => {
83
+ it('renders correctly', () => {
84
+ const { container } = render(<SkeletonPrice />);
85
+ const element = container.firstChild;
86
+ expect(element).toBeInTheDocument();
87
+ });
88
+
89
+ it('renders as a div element', () => {
90
+ const { container } = render(<SkeletonPrice />);
91
+ const element = container.firstChild;
92
+ if (element) {
93
+ expect(element.nodeName).toBe('DIV');
94
+ }
95
+ });
96
+
97
+ it('has styled component class', () => {
98
+ const { container } = render(<SkeletonPrice />);
99
+ const element = container.firstChild as HTMLElement;
100
+ expect(element.className).toMatch(/sc-/);
101
+ });
102
+
103
+ it('can be used multiple times', () => {
104
+ const { container } = render(
105
+ <>
106
+ <SkeletonPrice />
107
+ <SkeletonPrice />
108
+ <SkeletonPrice />
109
+ </>
110
+ );
111
+ const elements = container.querySelectorAll('div');
112
+ expect(elements.length).toBe(3);
113
+ });
114
+ });
115
+
116
+ describe('SkeletonButton', () => {
117
+ it('renders correctly', () => {
118
+ const { container } = render(<SkeletonButton as="div" />);
119
+ const element = container.firstChild;
120
+ expect(element).toBeInTheDocument();
121
+ });
122
+
123
+ it('renders as div when specified', () => {
124
+ const { container } = render(<SkeletonButton as="div" />);
125
+ const element = container.firstChild;
126
+ if (element) {
127
+ expect(element.nodeName).toBe('DIV');
128
+ }
129
+ });
130
+
131
+ it('has styled component class', () => {
132
+ const { container } = render(<SkeletonButton as="div" />);
133
+ const element = container.firstChild as HTMLElement;
134
+ expect(element.className).toMatch(/sc-/);
135
+ });
136
+
137
+ it('accepts additional props', () => {
138
+ const { container } = render(
139
+ <SkeletonButton as="div" data-testid="skeleton-button" />
140
+ );
141
+ const element = container.firstChild;
142
+ expect(element).toHaveAttribute('data-testid', 'skeleton-button');
143
+ });
144
+
145
+ it('is not interactive', () => {
146
+ const { container } = render(<SkeletonButton as="div" />);
147
+ const element = container.firstChild;
148
+ if (element) {
149
+ expect(element.nodeName).toBe('DIV');
150
+ }
151
+ });
152
+ });
153
+
154
+ describe('Skeleton Components Integration', () => {
155
+ it('all skeleton components render together', () => {
156
+ const { container } = render(
157
+ <div>
158
+ <SkeletonHeadline />
159
+ <SkeletonLine width="80px" />
160
+ <SkeletonPrice />
161
+ <SkeletonButton as="div" />
162
+ </div>
163
+ );
164
+
165
+ // Verify all components are present (4 skeleton components + 1 wrapper div = 5 divs minimum)
166
+ const elements = container.querySelectorAll('div');
167
+ expect(elements.length).toBeGreaterThanOrEqual(5);
168
+ });
169
+
170
+ it('skeleton components maintain consistent styling', () => {
171
+ const { container } = render(
172
+ <div>
173
+ <SkeletonLine />
174
+ <SkeletonHeadline />
175
+ <SkeletonPrice />
176
+ </div>
177
+ );
178
+
179
+ const styledElements = container.querySelectorAll('[class*="sc-"]');
180
+ expect(styledElements.length).toBeGreaterThanOrEqual(3);
181
+ });
182
+
183
+ it('skeleton components are div elements by default', () => {
184
+ const { container } = render(
185
+ <div>
186
+ <SkeletonLine />
187
+ <SkeletonHeadline />
188
+ <SkeletonPrice />
189
+ </div>
190
+ );
191
+
192
+ const divs = Array.from(container.querySelectorAll('div'));
193
+ expect(divs.length).toBeGreaterThanOrEqual(4);
194
+ });
195
+
196
+ it('props are correctly applied to skeleton components', () => {
197
+ const { container } = render(
198
+ <div>
199
+ <SkeletonLine width="50px" marginBottom="10px" />
200
+ <SkeletonButton as="div" className="btn-skeleton" />
201
+ </div>
202
+ );
203
+
204
+ const elements = container.querySelectorAll('div');
205
+ expect(elements.length).toBeGreaterThanOrEqual(2);
206
+ });
207
+ });
208
+
209
+ describe('Styled Components Structure', () => {
210
+ it('SkeletonLine has correct structure', () => {
211
+ const { container } = render(<SkeletonLine />);
212
+ expect(container.firstChild).toBeTruthy();
213
+ });
214
+
215
+ it('SkeletonHeadline has correct structure', () => {
216
+ const { container } = render(<SkeletonHeadline />);
217
+ expect(container.firstChild).toBeTruthy();
218
+ });
219
+
220
+ it('SkeletonPrice has correct structure', () => {
221
+ const { container } = render(<SkeletonPrice />);
222
+ expect(container.firstChild).toBeTruthy();
223
+ });
224
+
225
+ it('SkeletonButton has correct structure', () => {
226
+ const { container } = render(<SkeletonButton as="div" />);
227
+ expect(container.firstChild).toBeTruthy();
228
+ });
229
+ });
230
+
231
+ describe('Props and Variants', () => {
232
+ it('SkeletonLine with different widths', () => {
233
+ const { rerender, container } = render(<SkeletonLine width="20px" />);
234
+ expect(container.firstChild).toBeInTheDocument();
235
+
236
+ rerender(<SkeletonLine width="100px" />);
237
+ expect(container.firstChild).toBeInTheDocument();
238
+
239
+ rerender(<SkeletonLine width="50%" />);
240
+ expect(container.firstChild).toBeInTheDocument();
241
+ });
242
+
243
+ it('SkeletonLine with different margins', () => {
244
+ const { rerender, container } = render(
245
+ <SkeletonLine marginBottom="2px" />
246
+ );
247
+ expect(container.firstChild).toBeInTheDocument();
248
+
249
+ rerender(<SkeletonLine marginBottom="8px" />);
250
+ expect(container.firstChild).toBeInTheDocument();
251
+
252
+ rerender(<SkeletonLine marginBottom="16px" />);
253
+ expect(container.firstChild).toBeInTheDocument();
254
+ });
255
+ });
256
+ });
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+
3
+ export const BoatIcon = (props: any) => (
4
+ <svg
5
+ width="15"
6
+ height="16"
7
+ viewBox="0 0 15 16"
8
+ fill="none"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ {...props}
11
+ >
12
+ <path
13
+ d="M2.78887 3.51093V6.29999L6.98887 4.92187L11.1889 6.29999V3.51093H2.78887ZM1.34512 12.6L0.0326173 7.94062C0.0107423 7.89687 -0.000195216 7.82031 -0.000195216 7.71093C-0.000195216 7.40468 0.163867 7.18593 0.491992 7.05468L1.37793 6.75937V3.51093C1.37793 3.13906 1.52012 2.81093 1.80449 2.52656C2.08887 2.24218 2.41699 2.09999 2.78887 2.09999H4.88887V-7.20099e-06H9.08887V2.09999H11.1889C11.5607 2.09999 11.8889 2.24218 12.1732 2.52656C12.4576 2.81093 12.5998 3.13906 12.5998 3.51093V6.75937L13.4857 7.05468C13.9451 7.20781 14.0982 7.50312 13.9451 7.94062L12.6326 12.6H12.5998C11.5279 12.6 10.5873 12.1406 9.77793 11.2219C8.96856 12.1406 8.03887 12.6 6.98887 12.6C5.93887 12.6 5.00918 12.1406 4.19981 11.2219C3.39043 12.1406 2.4498 12.6 1.37793 12.6H1.34512ZM12.5998 14.0109H13.9779V15.4219H12.5998C11.6154 15.4219 10.6748 15.1922 9.77793 14.7328C8.90293 15.1703 7.97324 15.3891 6.98887 15.3891C6.00449 15.3891 5.07481 15.1703 4.19981 14.7328C3.30293 15.1922 2.3623 15.4219 1.37793 15.4219H-0.000195216V14.0109H1.37793C2.38418 14.0109 3.32481 13.7047 4.19981 13.0922C5.05293 13.6828 5.98262 13.9781 6.98887 13.9781C7.99512 13.9781 8.92481 13.6828 9.77793 13.0922C10.6529 13.7047 11.5936 14.0109 12.5998 14.0109Z"
14
+ fill="currentColor"
15
+ />
16
+ </svg>
17
+ );
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+
3
+ export const CalendarIcon = (props: any) => (
4
+ <svg
5
+ width="13"
6
+ height="14"
7
+ viewBox="0 0 13 14"
8
+ fill="none"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ {...props}
11
+ >
12
+ <path
13
+ d="M11.189 12.6V4.88907H1.41084V12.6H11.189ZM11.189 1.37813C11.5608 1.37813 11.889 1.52032 12.1733 1.80469C12.4577 2.08907 12.5999 2.41719 12.5999 2.78907V12.6C12.5999 12.9719 12.4577 13.3 12.1733 13.5844C11.889 13.8469 11.5608 13.9781 11.189 13.9781H1.41084C1.01709 13.9781 0.678028 13.8469 0.393653 13.5844C0.131152 13.3219 -9.75132e-05 12.9938 -9.75132e-05 12.6V2.78907C-9.75132e-05 2.41719 0.131152 2.08907 0.393653 1.80469C0.678028 1.52032 1.01709 1.37813 1.41084 1.37813H2.0999V5.05522e-06H3.51084V1.37813H9.08897V5.05522e-06H10.4999V1.37813H11.189ZM9.81084 6.30001V7.67813H8.3999V6.30001H9.81084ZM6.98897 6.30001V7.67813H5.61084V6.30001H6.98897ZM4.1999 6.30001V7.67813H2.78897V6.30001H4.1999Z"
14
+ fill="currentColor"
15
+ />
16
+ </svg>
17
+ );
@@ -0,0 +1,20 @@
1
+ import * as React from 'react';
2
+
3
+ export const ChevronRightIcon = (props: any) => (
4
+ <svg
5
+ width="16"
6
+ height="16"
7
+ viewBox="0 0 16 16"
8
+ fill="none"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ {...props}
11
+ >
12
+ <path
13
+ d="M6 12L10 8L6 4"
14
+ stroke="currentColor"
15
+ strokeWidth="1.5"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ />
19
+ </svg>
20
+ );
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+
3
+ export const LocationIcon = (props: any) => (
4
+ <svg
5
+ width="10"
6
+ height="14"
7
+ viewBox="0 0 10 14"
8
+ fill="none"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ {...props}
11
+ >
12
+ <path
13
+ d="M3.64238 6.13594C3.99238 6.46407 4.40801 6.62813 4.88926 6.62813C5.37051 6.62813 5.7752 6.46407 6.10332 6.13594C6.45332 5.78594 6.62832 5.37032 6.62832 4.88907C6.62832 4.40782 6.45332 4.00313 6.10332 3.67501C5.7752 3.32501 5.37051 3.15001 4.88926 3.15001C4.40801 3.15001 3.99238 3.32501 3.64238 3.67501C3.31426 4.00313 3.1502 4.40782 3.1502 4.88907C3.1502 5.37032 3.31426 5.78594 3.64238 6.13594ZM1.41113 1.44376C2.37363 0.481255 3.53301 5.05522e-06 4.88926 5.05522e-06C6.24551 5.05522e-06 7.39395 0.481255 8.33457 1.44376C9.29707 2.38438 9.77832 3.53282 9.77832 4.88907C9.77832 5.56719 9.60332 6.34376 9.25332 7.21876C8.9252 8.09376 8.52051 8.91407 8.03926 9.67969C7.55801 10.4453 7.07676 11.1672 6.59551 11.8453C6.13613 12.5016 5.74238 13.0266 5.41426 13.4203L4.88926 13.9781C4.75801 13.825 4.58301 13.6281 4.36426 13.3875C4.14551 13.125 3.75176 12.6219 3.18301 11.8781C2.61426 11.1125 2.11113 10.3797 1.67363 9.67969C1.25801 8.95782 0.875196 8.14844 0.525196 7.25157C0.175196 6.35469 0.000195552 5.56719 0.000195552 4.88907C0.000195552 3.53282 0.470508 2.38438 1.41113 1.44376Z"
14
+ fill="currentColor"
15
+ />
16
+ </svg>
17
+ );
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+
3
+ export const MoonIcon = (props: any) => (
4
+ <svg
5
+ width="14"
6
+ height="15"
7
+ viewBox="0 0 14 15"
8
+ fill="none"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ {...props}
11
+ >
12
+ <path
13
+ d="M7.21856 0.0656178C6.69356 0.984368 6.38731 1.92499 6.29981 2.88749C6.21231 3.84999 6.31074 4.77968 6.59512 5.67656C6.90137 6.57343 7.36074 7.38281 7.97324 8.10468C8.58574 8.82656 9.31856 9.40624 10.1717 9.84374C11.0467 10.2812 12.0092 10.5109 13.0592 10.5328C12.6436 11.2328 12.1295 11.8453 11.517 12.3703C10.9045 12.8953 10.2154 13.3109 9.44981 13.6172C8.68418 13.9016 7.86387 14.0437 6.98887 14.0437C6.02637 14.0437 5.11856 13.8687 4.26543 13.5187C3.43418 13.1469 2.69043 12.6437 2.03418 12.0094C1.3998 11.3531 0.89668 10.6094 0.524805 9.77812C0.174805 8.92499 -0.000195216 8.01718 -0.000195216 7.05468C-0.000195216 6.07031 0.185742 5.15156 0.557617 4.29843C0.929492 3.44531 1.44355 2.70156 2.0998 2.06718C2.75605 1.41093 3.52168 0.907805 4.39668 0.557805C5.27168 0.18593 6.21231 0.0218678 7.21856 0.0656178Z"
14
+ fill="currentColor"
15
+ />
16
+ </svg>
17
+ );
@@ -0,0 +1,7 @@
1
+ import { BoatIcon } from './BoatIcon';
2
+ import { CalendarIcon } from './CalendarIcon';
3
+ import { ChevronRightIcon } from './ChevronRightIcon';
4
+ import { LocationIcon } from './LocationIcon';
5
+ import { MoonIcon } from './MoonIcon';
6
+
7
+ export { BoatIcon, LocationIcon, CalendarIcon, MoonIcon, ChevronRightIcon };
@@ -0,0 +1,150 @@
1
+ import { TripCardApiData, ApiCruiseResult } from './types';
2
+
3
+ const decodeHtmlEntities = (text: string): string => {
4
+ const textarea = document.createElement('textarea');
5
+ textarea.innerHTML = text;
6
+ return textarea.value;
7
+ };
8
+
9
+ export const transformApiResult = (
10
+ result: ApiCruiseResult
11
+ ): TripCardApiData => {
12
+ const formatPrice = (price: number | string): string => {
13
+ if (price === 0 || price === 'Enquire now') {
14
+ return 'Enquire now';
15
+ }
16
+ return typeof price === 'string' ? price : `£${price}`;
17
+ };
18
+
19
+ const formatRoute = (itinerary: string[]): string => {
20
+ if (itinerary.length <= 2) {
21
+ return itinerary.join(' → ');
22
+ }
23
+ return `${itinerary[0]} → ${itinerary[itinerary.length - 1]}`;
24
+ };
25
+
26
+ const formatOriginalPrice = (
27
+ wasPrice: number | string
28
+ ): string | undefined => {
29
+ if (!wasPrice || wasPrice === 0 || wasPrice === 'Enquire now') {
30
+ return undefined;
31
+ }
32
+ return typeof wasPrice === 'string' ? wasPrice : `£${wasPrice}`;
33
+ };
34
+
35
+ const findCheapestCabinType = (): keyof typeof result.was_prices | null => {
36
+ const cheapestPrice = result.prices.cheapest_price;
37
+ const cabinTypes: Array<keyof typeof result.was_prices> = [
38
+ 'inside',
39
+ 'outside',
40
+ 'balcony',
41
+ 'suite'
42
+ ];
43
+
44
+ for (const cabinType of cabinTypes) {
45
+ const cabinPrice = result.prices[cabinType];
46
+ if (Number(cabinPrice) === Number(cheapestPrice)) {
47
+ return cabinType;
48
+ }
49
+ }
50
+ return null;
51
+ };
52
+
53
+ const getOriginalPrice = (): string | undefined => {
54
+ const cheapestCabinType = findCheapestCabinType();
55
+ if (cheapestCabinType && result.was_prices[cheapestCabinType]) {
56
+ return formatOriginalPrice(result.was_prices[cheapestCabinType]);
57
+ }
58
+ return undefined;
59
+ };
60
+
61
+ return {
62
+ cruise_id: String(result.cruise_id),
63
+ image: result.ship.image,
64
+ offer_label: result.campaigns.length > 0 ? result.campaigns[0] : undefined,
65
+ headline: decodeHtmlEntities(result.cruise_title),
66
+ date: result.extras.date,
67
+ duration: result.extras.duration,
68
+ route: formatRoute(result.itinerary),
69
+ ship: result.ship.name,
70
+ original_price: getOriginalPrice(),
71
+ price: formatPrice(result.prices.cheapest_price),
72
+ logo: result.cruise_line.logo,
73
+ logo_url: result.cruise_line.link,
74
+ cta_url: result.link,
75
+ cta_text: 'View Itinerary'
76
+ };
77
+ };
78
+
79
+ export const decodeIds = (encoded: string): number[] => {
80
+ try {
81
+ const decoded = atob(encoded);
82
+ const data = JSON.parse(decoded);
83
+ if (Array.isArray(data)) {
84
+ return data;
85
+ }
86
+ return [];
87
+ } catch (e) {
88
+ // Failed to decode trip card IDs
89
+ return [];
90
+ }
91
+ };
92
+
93
+ const isValidOffer = (result: ApiCruiseResult): boolean => {
94
+ if (result.departs) {
95
+ const departDate = new Date(result.departs);
96
+
97
+ if (isNaN(departDate.getTime())) {
98
+ return false;
99
+ }
100
+
101
+ departDate.setHours(0, 0, 0, 0);
102
+
103
+ const today = new Date();
104
+ today.setHours(0, 0, 0, 0);
105
+
106
+ if (departDate < today) {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ return true;
112
+ };
113
+
114
+ export const fetchCruiseCards = async (
115
+ cruiseIds: number[]
116
+ ): Promise<TripCardApiData[]> => {
117
+ const formData = new FormData();
118
+ formData.append('action', 'results');
119
+ formData.append('cruise_ids', cruiseIds.join(','));
120
+
121
+ const response = await fetch(
122
+ 'https://www.staging-thetimes.com/holidays/wp-admin/admin-ajax.php',
123
+ {
124
+ method: 'POST',
125
+ body: formData
126
+ }
127
+ );
128
+
129
+ if (!response.ok) {
130
+ throw new Error(`HTTP error! status: ${response.status}`);
131
+ }
132
+
133
+ const data = await response.json();
134
+
135
+ if (data.results && Array.isArray(data.results)) {
136
+ const validResults = data.results.filter((result: ApiCruiseResult) =>
137
+ isValidOffer(result)
138
+ );
139
+
140
+ const transformedCards: TripCardApiData[] = validResults.map(
141
+ (result: ApiCruiseResult) => transformApiResult(result)
142
+ );
143
+
144
+ return cruiseIds
145
+ .map(id => transformedCards.find(card => card.cruise_id === String(id)))
146
+ .filter(Boolean) as TripCardApiData[];
147
+ }
148
+
149
+ throw new Error('Invalid API response format');
150
+ };
@@ -0,0 +1,119 @@
1
+ import React, { FC, useState, useEffect } from 'react';
2
+ import { TripCardsProps, TripCardApiData } from './types';
3
+ import { TripCard } from './TripCard';
4
+ import { SkeletonCard } from './SkeletonCard';
5
+ import { TripCardsLayout } from './TripCardsLayout';
6
+ import { getMockTripCards } from './mockData';
7
+ import { decodeIds, fetchCruiseCards } from './helpers';
8
+
9
+ export const TripCards: FC<TripCardsProps> = ({
10
+ element,
11
+ useMockData = false,
12
+ widthContainerConfig,
13
+ widthItemConfig,
14
+ maxWidthItemConfig,
15
+ imgHeight,
16
+ forceStaticGrid
17
+ }) => {
18
+ const [cards, setCards] = useState<TripCardApiData[]>([]);
19
+ const [loading, setLoading] = useState(true);
20
+ const [error, setError] = useState<string | null>(null);
21
+ const [itemsPerPage, setItemsPerPage] = useState(2);
22
+ const allIds = [...decodeIds(element.tripcards || '')].filter(
23
+ (id, index, self) => self.indexOf(id) === index
24
+ );
25
+
26
+ useEffect(
27
+ () => {
28
+ const loadCards = async () => {
29
+ if (allIds.length === 0) {
30
+ setLoading(false);
31
+ setError('No valid cruise IDs provided');
32
+ return;
33
+ }
34
+
35
+ if (useMockData) {
36
+ setCards(getMockTripCards(allIds.length));
37
+ setLoading(false);
38
+ return;
39
+ }
40
+
41
+ try {
42
+ const transformedCards = await fetchCruiseCards(allIds);
43
+ setCards(transformedCards);
44
+ } catch (e) {
45
+ setError(e instanceof Error ? e.message : 'Failed to load cruises');
46
+ } finally {
47
+ setLoading(false);
48
+ }
49
+ };
50
+
51
+ loadCards();
52
+ },
53
+ [element.tripcards, useMockData]
54
+ );
55
+
56
+ useEffect(() => {
57
+ const handleResize = () => {
58
+ const width = window.innerWidth;
59
+ // sm/xs: < 768px → 1 card per page
60
+ // md/lg/xl: >= 768px → 2 cards per page
61
+ if (width < 768) {
62
+ setItemsPerPage(1);
63
+ } else {
64
+ setItemsPerPage(2);
65
+ }
66
+ };
67
+ handleResize();
68
+ window.addEventListener('resize', handleResize);
69
+ return () => window.removeEventListener('resize', handleResize);
70
+ }, []);
71
+
72
+ if (loading && allIds.length > 1) {
73
+ const skeletonItems = Array.from(
74
+ { length: allIds.length || 2 },
75
+ (_, i) => ({
76
+ id: `skeleton-${i}`
77
+ })
78
+ );
79
+
80
+ return (
81
+ <TripCardsLayout
82
+ element={element}
83
+ items={skeletonItems}
84
+ CardComponent={SkeletonCard}
85
+ itemsPerPage={itemsPerPage}
86
+ widthContainerConfig={widthContainerConfig}
87
+ widthItemConfig={widthItemConfig}
88
+ maxWidthItemConfig={maxWidthItemConfig}
89
+ imgHeight={imgHeight}
90
+ forceStaticGrid={forceStaticGrid}
91
+ />
92
+ );
93
+ }
94
+
95
+ if (error || allIds.length < 2 || cards.length < 2) {
96
+ return null;
97
+ }
98
+
99
+ const cardItems = cards.map((card, index) => ({
100
+ id: `${card.cruise_id}-${index}`,
101
+ data: card
102
+ }));
103
+
104
+ return (
105
+ <TripCardsLayout
106
+ element={element}
107
+ items={cardItems}
108
+ CardComponent={TripCard}
109
+ itemsPerPage={itemsPerPage}
110
+ widthContainerConfig={widthContainerConfig}
111
+ widthItemConfig={widthItemConfig}
112
+ maxWidthItemConfig={maxWidthItemConfig}
113
+ imgHeight={imgHeight}
114
+ forceStaticGrid={forceStaticGrid}
115
+ />
116
+ );
117
+ };
118
+
119
+ export default TripCards;