@times-components/ts-components 1.104.1-alpha.14 → 1.104.1-alpha.34

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 (31) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/components/article-header/ArticleHeader.js +6 -3
  3. package/dist/components/article-header/ArticleHeader.stories.js +2 -5
  4. package/dist/components/article-sidebar/ArticleSidebar.js +6 -3
  5. package/dist/components/article-sidebar/ArticleSidebar.stories.js +20 -10
  6. package/dist/components/article-sidebar/__tests__/index.test.js +49 -7
  7. package/dist/components/article-sidebar/tracking-helpers.d.ts +13 -0
  8. package/dist/components/article-sidebar/tracking-helpers.js +16 -0
  9. package/dist/components/twitter-embed/TwitterEmbed.d.ts +5 -3
  10. package/dist/components/twitter-embed/TwitterEmbed.js +85 -12
  11. package/dist/components/twitter-embed/__tests__/TwitterEmbed.test.js +366 -25
  12. package/dist/components/twitter-embed/styles.d.ts +8 -0
  13. package/dist/components/twitter-embed/styles.js +75 -0
  14. package/dist/components/updated-timestamp/UpdatedTimestamp.js +4 -2
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +2 -1
  17. package/package.json +5 -4
  18. package/rnw.js +1 -1
  19. package/src/components/article-header/ArticleHeader.stories.tsx +1 -5
  20. package/src/components/article-header/ArticleHeader.tsx +5 -6
  21. package/src/components/article-sidebar/ArticleSidebar.stories.tsx +24 -10
  22. package/src/components/article-sidebar/ArticleSidebar.tsx +22 -2
  23. package/src/components/article-sidebar/__tests__/__snapshots__/index.test.tsx.snap +8 -8
  24. package/src/components/article-sidebar/__tests__/index.test.tsx +56 -6
  25. package/src/components/article-sidebar/tracking-helpers.ts +21 -0
  26. package/src/components/twitter-embed/TwitterEmbed.tsx +146 -14
  27. package/src/components/twitter-embed/__tests__/TwitterEmbed.test.tsx +523 -22
  28. package/src/components/twitter-embed/styles.ts +82 -0
  29. package/src/components/updated-timestamp/UpdatedTimestamp.tsx +3 -1
  30. package/src/index.ts +2 -0
  31. package/tslint.json +2 -1
@@ -1,11 +1,7 @@
1
1
  import React from 'react';
2
2
  import { storiesOf } from '@storybook/react';
3
3
  import { select, text } from '@storybook/addon-knobs';
4
- import {
5
- // format,
6
- addMinutes
7
- } from 'date-fns';
8
- // import { utcToZonedTime } from 'date-fns-tz';
4
+ import addMinutes from 'date-fns/addMinutes';
9
5
 
10
6
  import { ArticleHarness } from '../../fixtures/article-harness/ArticleHarness';
11
7
  import ArticleHeader from './ArticleHeader';
@@ -1,10 +1,9 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import {
3
- differenceInSeconds,
4
- differenceInCalendarDays,
5
- formatDistanceStrict
6
- } from 'date-fns';
7
- import { format, utcToZonedTime } from 'date-fns-tz';
2
+ import differenceInSeconds from 'date-fns/differenceInSeconds';
3
+ import differenceInCalendarDays from 'date-fns/differenceInCalendarDays';
4
+ import formatDistanceStrict from 'date-fns/formatDistanceStrict';
5
+ import utcToZonedTime from 'date-fns-tz/utcToZonedTime';
6
+ import format from 'date-fns-tz/format';
8
7
 
9
8
  import { BreakingArticleFlag } from '../article-flag/LiveArticleFlag';
10
9
  import safeDecodeURIComponent from '../../utils/safeDecodeURIComponent';
@@ -1,30 +1,32 @@
1
1
  import React from 'react';
2
2
  import { storiesOf } from '@storybook/react';
3
3
  import { ArticleSidebar } from './ArticleSidebar';
4
+ import { TrackingContextProvider } from '../../helpers/tracking/TrackingContextProvider';
5
+ import analyticsStream from '../../fixtures/analytics-actions/analytics-actions';
4
6
 
5
7
  const getAttributes = () => {
6
8
  const sectionTitle = 'Puzzles';
7
9
  const data = [
8
10
  {
9
11
  title: 'Crossword',
10
- url: 'https://www.thetimes.co.uk/puzzles/crossword',
12
+ url: 'https://www.thetimes.com/puzzles/crossword',
11
13
  imgUrl:
12
- 'https://www.thetimes.co.uk/d/img/puzzles/new-illustrations/crossword-c7ae8934ef.png'
14
+ 'https://www.thetimes.com/d/img/puzzles/new-illustrations/crossword-c7ae8934ef.png'
13
15
  },
14
16
  {
15
17
  title: 'Polygon',
16
- url: 'https://www.thetimes.co.uk/puzzles/word-puzzles',
18
+ url: 'https://www.thetimes.com/puzzles/word-puzzles',
17
19
  imgUrl:
18
- 'https://www.thetimes.co.uk/d/img/puzzles/new-illustrations/polygon-875ea55487.png'
20
+ 'https://www.thetimes.com/d/img/puzzles/new-illustrations/polygon-875ea55487.png'
19
21
  },
20
22
  {
21
23
  title: 'Sudoku',
22
- url: 'https://www.thetimes.co.uk/puzzles/sudoku',
24
+ url: 'https://www.thetimes.com/puzzles/sudoku',
23
25
  imgUrl:
24
- 'https://www.thetimes.co.uk/d/img/puzzles/new-illustrations/sudoku-ee2aea0209.png'
26
+ 'https://www.thetimes.com/d/img/puzzles/new-illustrations/sudoku-ee2aea0209.png'
25
27
  }
26
28
  ];
27
- const pageLink = 'https://www.thetimes.co.uk/puzzles';
29
+ const pageLink = 'https://www.thetimes.com/puzzles';
28
30
 
29
31
  return { sectionTitle, data, pageLink };
30
32
  };
@@ -35,9 +37,21 @@ storiesOf('Typescript Component/Article Sidebar', module).add(
35
37
  const props = getAttributes();
36
38
 
37
39
  return (
38
- <div style={{ maxWidth: '204px' }}>
39
- <ArticleSidebar {...props} />
40
- </div>
40
+ <TrackingContextProvider
41
+ context={{
42
+ component: 'ArticleSkeleton',
43
+ object: 'ArticleSidebar',
44
+ attrs: {
45
+ article_name: 'articleHeadline',
46
+ section_details: 'section'
47
+ }
48
+ }}
49
+ analyticsStream={analyticsStream}
50
+ >
51
+ <div style={{ maxWidth: '204px' }}>
52
+ <ArticleSidebar {...props} />
53
+ </div>
54
+ </TrackingContextProvider>
41
55
  );
42
56
  }
43
57
  );
@@ -13,6 +13,8 @@ import {
13
13
  Title,
14
14
  TitleIconContainer
15
15
  } from './styles';
16
+ import { useTrackingContext } from '../../helpers/tracking/TrackingContextProvider';
17
+ import { handleClick } from './tracking-helpers';
16
18
 
17
19
  export interface ArticleSideBarProps {
18
20
  sectionTitle: string;
@@ -25,9 +27,17 @@ export const ArticleSidebar: FC<ArticleSideBarProps> = ({
25
27
  data,
26
28
  pageLink
27
29
  }) => {
30
+ const { fireAnalyticsEvent } = useTrackingContext();
31
+
28
32
  return (
29
33
  <Container>
30
- <Link href={pageLink}>
34
+ <Link
35
+ href={pageLink}
36
+ onClick={() =>
37
+ handleClick(fireAnalyticsEvent, 'puzzle sidebar: header selected')
38
+ }
39
+ className="trigger"
40
+ >
31
41
  <TitleIconContainer>
32
42
  <Title>{sectionTitle}</Title>
33
43
  <ChevronButton>
@@ -41,7 +51,17 @@ export const ArticleSidebar: FC<ArticleSideBarProps> = ({
41
51
 
42
52
  {data.map(({ title, url, imgUrl }) => (
43
53
  <React.Fragment key={title}>
44
- <PuzzleContainer href={url}>
54
+ <PuzzleContainer
55
+ href={url}
56
+ onClick={() =>
57
+ handleClick(
58
+ fireAnalyticsEvent,
59
+ 'puzzle sidebar: puzzle selected',
60
+ `${title}`
61
+ )
62
+ }
63
+ className="trigger-card-link"
64
+ >
45
65
  <PuzzleImage src={imgUrl} alt="Puzzle thumbnail" />
46
66
  <ItemTitle>{title}</ItemTitle>
47
67
  </PuzzleContainer>
@@ -6,8 +6,8 @@ exports[`ArticleSidebar should render ArticleSidebar component 1`] = `
6
6
  class="sc-bwzfXH kzdaiS"
7
7
  >
8
8
  <a
9
- class="sc-ifAKCX kcwwi"
10
- href="https://www.thetimes.co.uk/puzzles"
9
+ class="trigger sc-ifAKCX kcwwi"
10
+ href="https://www.thetimes.com/puzzles"
11
11
  >
12
12
  <div
13
13
  class="sc-EHOje bmFGSY"
@@ -48,13 +48,13 @@ exports[`ArticleSidebar should render ArticleSidebar component 1`] = `
48
48
  class="sc-bxivhb euCQXs"
49
49
  />
50
50
  <a
51
- class="sc-dnqmqq gzKzfS"
52
- href="https://www.thetimes.co.uk/puzzles/crossword"
51
+ class="trigger-card-link sc-dnqmqq gzKzfS"
52
+ href="https://www.thetimes.com/puzzles/crossword"
53
53
  >
54
54
  <img
55
55
  alt="Puzzle thumbnail"
56
56
  class="sc-iwsKbI elYeaz"
57
- src="https://www.thetimes.co.uk/imageserver/image/%2Fpuzzles%2Ficons%2F33b27655-dcc9-421f-906f-b2b10dd26865.png?crop=1250%2C833%2C0%2C0&resize=500"
57
+ src="https://www.thetimes.com/imageserver/image/%2Fpuzzles%2Ficons%2F33b27655-dcc9-421f-906f-b2b10dd26865.png?crop=1250%2C833%2C0%2C0&resize=500"
58
58
  />
59
59
  <p
60
60
  class="sc-htoDjs bXFtLv"
@@ -66,13 +66,13 @@ exports[`ArticleSidebar should render ArticleSidebar component 1`] = `
66
66
  class="sc-bxivhb euCQXs"
67
67
  />
68
68
  <a
69
- class="sc-dnqmqq gzKzfS"
70
- href="https://www.thetimes.co.uk/puzzles/sudoku"
69
+ class="trigger-card-link sc-dnqmqq gzKzfS"
70
+ href="https://www.thetimes.com/puzzles/sudoku"
71
71
  >
72
72
  <img
73
73
  alt="Puzzle thumbnail"
74
74
  class="sc-iwsKbI elYeaz"
75
- src="https://www.thetimes.co.uk/imageserver/image/%2Fpuzzles%2Ficons%2F33b27655-dcc9-421f-906f-b2b10dd26865.png?crop=1250%2C833%2C0%2C0&resize=500"
75
+ src="https://www.thetimes.com/imageserver/image/%2Fpuzzles%2Ficons%2F33b27655-dcc9-421f-906f-b2b10dd26865.png?crop=1250%2C833%2C0%2C0&resize=500"
76
76
  />
77
77
  <p
78
78
  class="sc-htoDjs bXFtLv"
@@ -1,33 +1,83 @@
1
1
  import React from 'react';
2
2
  import { ArticleSidebar, ArticleSideBarProps } from '../ArticleSidebar';
3
- import { render } from '@testing-library/react';
3
+ import { render, fireEvent } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
+ import { useTrackingContext } from '../../../helpers/tracking/TrackingContextProvider';
6
+
7
+ jest.mock('../../../helpers/tracking/TrackingContextProvider', () => ({
8
+ useTrackingContext: jest.fn()
9
+ }));
10
+
11
+ const mockFireAnalyticsEvent = jest.fn();
5
12
 
6
13
  const defaultProps: ArticleSideBarProps = {
7
14
  sectionTitle: 'Puzzles for you',
8
15
  data: [
9
16
  {
10
17
  title: 'Crossword',
11
- url: 'https://www.thetimes.co.uk/puzzles/crossword',
18
+ url: 'https://www.thetimes.com/puzzles/crossword',
12
19
  imgUrl:
13
- 'https://www.thetimes.co.uk/imageserver/image/%2Fpuzzles%2Ficons%2F33b27655-dcc9-421f-906f-b2b10dd26865.png?crop=1250%2C833%2C0%2C0&resize=500'
20
+ 'https://www.thetimes.com/imageserver/image/%2Fpuzzles%2Ficons%2F33b27655-dcc9-421f-906f-b2b10dd26865.png?crop=1250%2C833%2C0%2C0&resize=500'
14
21
  },
15
22
  {
16
23
  title: 'Polygon',
17
- url: 'https://www.thetimes.co.uk/puzzles/sudoku',
24
+ url: 'https://www.thetimes.com/puzzles/sudoku',
18
25
  imgUrl:
19
- 'https://www.thetimes.co.uk/imageserver/image/%2Fpuzzles%2Ficons%2F33b27655-dcc9-421f-906f-b2b10dd26865.png?crop=1250%2C833%2C0%2C0&resize=500'
26
+ 'https://www.thetimes.com/imageserver/image/%2Fpuzzles%2Ficons%2F33b27655-dcc9-421f-906f-b2b10dd26865.png?crop=1250%2C833%2C0%2C0&resize=500'
20
27
  }
21
28
  ],
22
- pageLink: 'https://www.thetimes.co.uk/puzzles'
29
+ pageLink: 'https://www.thetimes.com/puzzles'
23
30
  };
24
31
 
25
32
  const renderComponent = (props: ArticleSideBarProps) =>
26
33
  render(<ArticleSidebar {...props} />);
27
34
 
28
35
  describe('ArticleSidebar', () => {
36
+ beforeEach(() => {
37
+ (useTrackingContext as jest.Mock).mockReturnValue({
38
+ fireAnalyticsEvent: mockFireAnalyticsEvent
39
+ });
40
+ });
41
+
42
+ afterEach(() => {
43
+ jest.clearAllMocks();
44
+ });
45
+
29
46
  it('should render ArticleSidebar component', () => {
30
47
  const { asFragment } = renderComponent(defaultProps);
31
48
  expect(asFragment()).toMatchSnapshot();
32
49
  });
50
+
51
+ it('should call fireAnalyticsEvent when header is clicked', () => {
52
+ const { container } = renderComponent(defaultProps);
53
+ fireEvent.click(container.querySelector('.trigger')!);
54
+
55
+ expect(mockFireAnalyticsEvent).toHaveBeenCalledWith({
56
+ object: 'ArticleSidebar',
57
+ action: 'Clicked',
58
+ attrs: {
59
+ event_navigation_action: 'navigation',
60
+ event_navigation_name: 'puzzle sidebar: header selected',
61
+ event_navigation_browsing_method: 'click',
62
+ component_name: 'Article Sidebar'
63
+ }
64
+ });
65
+ });
66
+
67
+ it('should call fireAnalyticsEvent when a puzzle card is clicked', () => {
68
+ const { container } = renderComponent(defaultProps);
69
+ fireEvent.click(container.querySelector('.trigger-card-link')!);
70
+
71
+ expect(mockFireAnalyticsEvent).toHaveBeenCalledWith({
72
+ object: 'ArticleSidebar',
73
+ action: 'Clicked',
74
+ attrs: {
75
+ event_navigation_action: 'navigation',
76
+ event_navigation_name: 'puzzle sidebar: puzzle selected',
77
+ event_navigation_browsing_method: 'click',
78
+ component_name: 'Article Sidebar',
79
+ article_parent_name: 'crossword'
80
+ }
81
+ });
82
+ });
33
83
  });
@@ -0,0 +1,21 @@
1
+ const clickEvent = (title: string, parent: string = '') => ({
2
+ object: 'ArticleSidebar',
3
+ action: 'Clicked',
4
+ attrs: {
5
+ event_navigation_action: 'navigation',
6
+ event_navigation_name: `${title}`,
7
+ event_navigation_browsing_method: 'click',
8
+ component_name: 'Article Sidebar',
9
+ ...(parent && { article_parent_name: parent.toLowerCase() })
10
+ }
11
+ });
12
+
13
+ const handleClick = (
14
+ fireAnalyticsEvent: ((analyticsEvent: any) => void) | undefined,
15
+ title: string,
16
+ parent?: string
17
+ ) => {
18
+ fireAnalyticsEvent && fireAnalyticsEvent(clickEvent(title, parent));
19
+ };
20
+
21
+ export { handleClick, clickEvent };
@@ -1,4 +1,23 @@
1
- import React, { useEffect } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ AllowButton,
4
+ CardContainer,
5
+ EnableButton,
6
+ LinkPrivacyManager,
7
+ Paragraph,
8
+ Title,
9
+ CustomIconContainer,
10
+ Header
11
+ } from './styles';
12
+ import get from 'lodash.get';
13
+ import { InfoIcon } from '../inline-message/InfoIcon';
14
+ // @ts-ignore
15
+ import InteractiveWrapper from '@times-components/interactive-wrapper';
16
+
17
+ export declare type TwitterEmbedProps = {
18
+ element: any;
19
+ url: string;
20
+ };
2
21
 
3
22
  declare global {
4
23
  interface Window {
@@ -10,31 +29,144 @@ declare global {
10
29
  }
11
30
  }
12
31
 
13
- export const TwitterEmbed: React.FC<{
14
- sectionColour: string;
15
- }> = () => {
16
- // tslint:disable-next-line:no-console
17
- console.log('window', window);
32
+ export const TwitterEmbed: React.FC<TwitterEmbedProps> = ({ element, url }) => {
33
+ const [allowedOnce, setAllowedOnce] = useState(false);
34
+ const [isTwitterAllowed, setIsTwitterAllowed] = useState(false);
18
35
 
19
36
  useEffect(() => {
20
37
  if (window.__tcfapi) {
21
38
  window.__tcfapi('getCustomVendorConsents', 2, (data, success) => {
22
- if (success) {
23
- // tslint:disable-next-line:no-console
24
- console.log('TCF API response:', data);
39
+ if (success && data && data.consentedVendors) {
40
+ const isTwitterVendorAllowed = data.consentedVendors.some(
41
+ (vendor: { name: string }) => vendor.name === 'Twitter'
42
+ );
43
+ setIsTwitterAllowed(isTwitterVendorAllowed);
25
44
  } else {
26
45
  // tslint:disable-next-line:no-console
27
- console.log('Error fetching TCF API data');
46
+ console.log('Error fetching consent data or Twitter not allowed');
28
47
  }
29
48
  });
49
+ }
50
+ // tslint:disable-next-line:no-console
51
+ console.log('window', window);
52
+ }, []);
53
+
54
+ enum ModalType {
55
+ GDPR = 'gdpr',
56
+ CCPA = 'ccpa'
57
+ }
58
+
59
+ const openPrivacyModal = (type: ModalType, messageId: string) => {
60
+ const loadModal = get(window, `_sp_.${type}.loadPrivacyManagerModal`);
61
+
62
+ if (loadModal) {
63
+ loadModal(messageId);
30
64
  } else {
31
65
  // tslint:disable-next-line:no-console
32
- console.log('TCF API not available');
66
+ console.warn('Sourcepoint LoadPrivacyManagerModal is not available');
33
67
  }
34
- }, []);
68
+ };
69
+
70
+ const allowCookiesOnce = () => {
71
+ setAllowedOnce(true);
72
+ setIsTwitterAllowed(true);
73
+ };
35
74
 
75
+ const handlePrivacyManagerClick = (
76
+ e: React.MouseEvent<HTMLAnchorElement>
77
+ ) => {
78
+ e.preventDefault();
79
+ openPrivacyModal(
80
+ ModalType.GDPR,
81
+ window.__TIMES_CONFIG__.sourcepoint.gdprMessageId
82
+ );
83
+ };
84
+
85
+ const socialMediaVendors = {
86
+ twitter: { id: '5fab0c31a22863611c5f8764', status: 'pending' }
87
+ };
88
+
89
+ const enableCookies = () => {
90
+ const onCustomConsent = (data: any, success: boolean) => {
91
+ if (success) {
92
+ // tslint:disable-next-line:no-console
93
+ console.log('Consent successfully updated', data);
94
+ setIsTwitterAllowed(true);
95
+ } else {
96
+ // tslint:disable-next-line:no-console
97
+ console.error('Failed to set consent for Twitter.', data);
98
+ }
99
+ };
100
+
101
+ const twitterVendorId = socialMediaVendors.twitter.id;
102
+
103
+ if (window.__tcfapi && twitterVendorId) {
104
+ window.__tcfapi(
105
+ 'getCustomVendorConsents',
106
+ 2,
107
+ (data: any, successful: boolean) => {
108
+ if (successful && data && data.grants[twitterVendorId]) {
109
+ // const purposeGrants = Object.keys(data.grants[twitterVendorId].purposeGrants).join(',');
110
+
111
+ (window.__tcfapi as any)(
112
+ 'postCustomConsent',
113
+ 2,
114
+ onCustomConsent,
115
+ twitterVendorId,
116
+ Object.keys(data.grants[twitterVendorId].purposeGrants),
117
+ ''
118
+ );
119
+ } else {
120
+ // tslint:disable-next-line:no-console
121
+ console.error('Twitter vendor consent not available:', data);
122
+ }
123
+ }
124
+ );
125
+ } else {
126
+ // tslint:disable-next-line:no-console
127
+ console.error(
128
+ 'TCF API is not available or Twitter vendor ID is missing.'
129
+ );
130
+ }
131
+ };
132
+
133
+ // tslint:disable-next-line:no-console
134
+ console.log('allowedOnce', allowedOnce);
135
+ // tslint:disable-next-line:no-console
136
+ console.log(
137
+ 'allowedOnce || isTwitterAllowed',
138
+ allowedOnce || isTwitterAllowed
139
+ );
36
140
  // tslint:disable-next-line:no-console
37
- console.log('window.__tcfapi', window.__tcfapi);
141
+ console.log(
142
+ 'allowedOnce && isTwitterAllowed',
143
+ allowedOnce && isTwitterAllowed
144
+ );
38
145
 
39
- return <h1>Test</h1>;
146
+ return isTwitterAllowed ? (
147
+ <InteractiveWrapper
148
+ attributes={element.attributes}
149
+ element={element.value}
150
+ key={element.key}
151
+ source={url}
152
+ />
153
+ ) : (
154
+ <CardContainer>
155
+ <Header>
156
+ <CustomIconContainer>
157
+ <InfoIcon />
158
+ </CustomIconContainer>
159
+ <Title>X (Twitter) content blocked</Title>
160
+ </Header>
161
+ <Paragraph>
162
+ Please enable cookies and other technologies to view this content. You
163
+ can update your cookies preferences any time using{' '}
164
+ <LinkPrivacyManager href="#" onClick={handlePrivacyManagerClick}>
165
+ privacy manager.
166
+ </LinkPrivacyManager>
167
+ </Paragraph>
168
+ <EnableButton onClick={enableCookies}>Enable cookies</EnableButton>
169
+ <AllowButton onClick={allowCookiesOnce}>Allow cookies once</AllowButton>
170
+ </CardContainer>
171
+ );
40
172
  };