@financial-times/n-myft-ui 23.0.1 → 24.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. package/.circleci/config.yml +13 -16
  2. package/.circleci/shared-helpers/helper-npm-install-peer-deps +6 -5
  3. package/.github/settings.yml +1 -1
  4. package/.scss-lint.yml +3 -3
  5. package/Makefile +1 -0
  6. package/README.md +11 -8
  7. package/build-state/npm-shrinkwrap.json +11383 -10733
  8. package/components/collections/collections.html +3 -11
  9. package/components/concept-list/concept-list.html +1 -4
  10. package/components/csrf-token/__tests__/input.test.js +23 -0
  11. package/components/csrf-token/input.jsx +26 -0
  12. package/components/follow-button/__tests__/follow-button.test.js +40 -0
  13. package/components/follow-button/follow-button.jsx +174 -0
  14. package/components/instant-alert/instant-alert.html +1 -1
  15. package/components/pin-button/pin-button.html +1 -1
  16. package/components/save-for-later/save-for-later.html +6 -9
  17. package/components/unread-articles-indicator/README.md +2 -62
  18. package/components/unread-articles-indicator/date-fns.js +5 -12
  19. package/components/unread-articles-indicator/index.js +1 -62
  20. package/components/unread-articles-indicator/storage.js +0 -44
  21. package/demos/app.js +30 -3
  22. package/demos/templates/demo.html +2 -4
  23. package/demos/templates/demo.jsx +33 -0
  24. package/demos/templates/digest-on-follow.html +1 -1
  25. package/jest.config.js +8 -0
  26. package/mixins/lozenge/_themes.scss +8 -4
  27. package/mixins/lozenge/main.scss +50 -4
  28. package/myft/main.scss +0 -1
  29. package/myft/ui/index.js +0 -2
  30. package/package.json +20 -6
  31. package/test/unread-articles-indicator/index.spec.js +13 -65
  32. package/test/unread-articles-indicator/storage.spec.js +0 -93
  33. package/components/csrf-token/input.html +0 -5
  34. package/components/follow-button/follow-button.html +0 -79
  35. package/components/header-tooltip/index.js +0 -37
  36. package/components/header-tooltip/main.scss +0 -12
  37. package/components/unread-articles-indicator/constants.js +0 -4
  38. package/components/unread-articles-indicator/count-unread-articles.js +0 -26
  39. package/components/unread-articles-indicator/main.scss +0 -59
  40. package/components/unread-articles-indicator/tracking.js +0 -15
  41. package/components/unread-articles-indicator/ui.js +0 -99
  42. package/components/unread-articles-indicator/update-count.js +0 -40
  43. package/test/unread-articles-indicator/count-unread-articles.spec.js +0 -72
  44. package/test/unread-articles-indicator/tracking.spec.js +0 -26
  45. package/test/unread-articles-indicator/ui.spec.js +0 -123
  46. package/test/unread-articles-indicator/update-count.spec.js +0 -156
@@ -10,17 +10,9 @@
10
10
  {{#concepts}}
11
11
  <li class="collection__concept">
12
12
  {{#if ../liteStyle}}
13
- {{> n-myft-ui/components/follow-button/follow-button
14
- variant="primary"
15
- buttonText=name
16
- collectionName=../collectionName
17
- }}
13
+ {{{renderReactComponent localPath="components/follow-button/follow-button" variant="primary" buttonText=name flags=@root.flags collectionName=../collectionName}}}
18
14
  {{else}}
19
- {{> n-myft-ui/components/follow-button/follow-button
20
- variant="inverse"
21
- buttonText=name
22
- collectionName=../collectionName
23
- }}
15
+ {{{renderReactComponent localPath="components/follow-button/follow-button" variant="inverse" buttonText=name flags=@root.flags collectionName=../collectionName}}}
24
16
  {{/if}}
25
17
  </li>
26
18
  {{/concepts}}
@@ -50,7 +42,7 @@
50
42
  {{~/unless~}}
51
43
  {{~/concepts~}}"
52
44
  />
53
- {{> n-myft-ui/components/csrf-token/input}}
45
+ {{{renderReactComponent localPath="components/csrf-token/input"}}}
54
46
  <input
55
47
  type="hidden"
56
48
  name="name"
@@ -20,10 +20,7 @@
20
20
  class="concept-list__concept">
21
21
  {{prefLabel}}
22
22
  </a>
23
- {{> n-myft-ui/components/follow-button/follow-button
24
- conceptId=id
25
- name=prefLabel
26
- }}
23
+ {{{renderReactComponent localPath="components/follow-button/follow-button" conceptId=id name=prefLabel flags=@root.flags}}}
27
24
  </li>
28
25
  {{/each}}
29
26
  </ul>
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import CsrfToken from '../input';
3
+ import { render } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const props = {
7
+ cacheablePersonalisedUrl: false
8
+ };
9
+
10
+ describe('Csrf Token Input', () => {
11
+
12
+ test('It renders default button', async () => {
13
+ let { container } = render(<CsrfToken {...props} />);
14
+ expect(container.querySelector('[name=\'token\']')).toBeTruthy();
15
+ });
16
+
17
+ test('It renders csrf token attribute', async () => {
18
+ let { container } = render(<CsrfToken cacheablePersonalisedUrl={true} csrfToken={'test-token'} />);
19
+ expect(container.querySelector('[data-myft-csrf-token=\'test-token\']')).toBeTruthy();
20
+ });
21
+
22
+
23
+ });
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+
3
+ export default function CsrfToken ({ cacheablePersonalisedUrl, csrfToken }) {
4
+
5
+ let inputProps = {};
6
+
7
+ if (cacheablePersonalisedUrl) {
8
+ inputProps = {
9
+ ...inputProps,
10
+ 'data-myft-csrf-token': csrfToken
11
+ };
12
+ }
13
+
14
+ if(csrfToken) {
15
+ inputProps.value = csrfToken;
16
+ }
17
+
18
+ return (
19
+ <input
20
+ {...inputProps}
21
+ type="hidden"
22
+ name="token"
23
+ />
24
+ );
25
+
26
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import FollowButton from '../follow-button';
3
+ import { render, screen } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const props = {
7
+ flags: {
8
+ myFtApi: true,
9
+ myFtApiWrite: true
10
+ },
11
+ conceptId: '0000-000000-00000-0000',
12
+ name: 'Follow button'
13
+ };
14
+
15
+ describe('Follow button', () => {
16
+
17
+ test('It renders default button', async () => {
18
+ render(<FollowButton {...props} />);
19
+ expect(screen.findByText('Add to myFT')).toBeTruthy();
20
+ });
21
+
22
+ test('It renders a variant', async () => {
23
+ const { container } = render(<FollowButton {...props} variant={'standard'} />);
24
+ expect(container.getElementsByClassName('n-myft-follow-button--standard')).toHaveLength(1);
25
+ });
26
+
27
+ test('It renders follow button form', async () => {
28
+ const { container } = render(<FollowButton {...props} variant={'standard'} />);
29
+ expect(container.querySelector(`form[action='/myft/add/${props.conceptId}']`)).toBeTruthy();
30
+ });
31
+
32
+ test('Button state changes when attributes change', () => {
33
+ render(<FollowButton {...props}
34
+ variant={'standard'}
35
+ setFollowButtonStateToSelected={true}
36
+ cacheablePersonalisedUrl={true} />);
37
+ expect(screen.findByText('Added')).toBeTruthy();
38
+ });
39
+
40
+ });
@@ -0,0 +1,174 @@
1
+ import React from 'react';
2
+ import CsrfToken from '../csrf-token/input';
3
+
4
+ function generateFormProps (props) {
5
+ let generatedProps = {};
6
+
7
+ const {
8
+ collectionName,
9
+ followPlusDigestEmail,
10
+ conceptId,
11
+ setFollowButtonStateToSelected,
12
+ cacheablePersonalisedUrl
13
+ } = props;
14
+
15
+ if (collectionName) {
16
+ generatedProps['data-myft-tracking'] = `collectionName=${collectionName}`;
17
+ }
18
+
19
+ if(followPlusDigestEmail) {
20
+ generatedProps['action'] = `/__myft/api/core/follow-plus-digest-email/${conceptId}?method=put`;
21
+ generatedProps['data-myft-ui-variant'] = 'followPlusDigestEmail';
22
+ } else {
23
+ if(setFollowButtonStateToSelected && cacheablePersonalisedUrl) {
24
+ generatedProps['action'] = `/myft/remove/${conceptId}`;
25
+ generatedProps['data-js-action'] = `/__myft/api/core/followed/concept/${conceptId}?method=delete`;
26
+ } else {
27
+ generatedProps['action'] = `/myft/add/${conceptId}`;
28
+ generatedProps['data-js-action'] = `/__myft/api/core/followed/concept/${conceptId}?method=put`;
29
+ }
30
+ }
31
+
32
+ return generatedProps;
33
+
34
+ }
35
+
36
+ function generateButtonProps (props) {
37
+
38
+ const {
39
+ cacheablePersonalisedUrl,
40
+ setFollowButtonStateToSelected,
41
+ name,
42
+ buttonText,
43
+ variant,
44
+ conceptId,
45
+ alternateText,
46
+ followPlusDigestEmail
47
+ } = props;
48
+
49
+ let generatedProps = {
50
+ 'data-concept-id': conceptId,
51
+ 'n-myft-follow-button': 'true',
52
+ 'data-trackable': 'follow',
53
+ type: 'submit'
54
+ };
55
+
56
+ if (cacheablePersonalisedUrl && setFollowButtonStateToSelected) {
57
+ generatedProps['aria-label'] = `Remove ${name} from myFT`;
58
+ generatedProps['title'] = `Remove ${name} from myFT`
59
+ generatedProps['data-alternate-label'] = `Add ${name} to myFT`;
60
+ generatedProps['aria-pressed'] = true;
61
+
62
+ if(alternateText) {
63
+ generatedProps['data-alternate-text'] = alternateText;
64
+ } else {
65
+ if(buttonText) {
66
+ generatedProps['data-alternate-text'] = buttonText;
67
+ } else {
68
+ generatedProps['data-alternate-text'] = 'Add to myFT';
69
+ }
70
+ }
71
+ } else {
72
+ generatedProps['aria-pressed'] = false;
73
+ generatedProps['aria-label'] = `Add ${name} to myFT`;
74
+ generatedProps['title'] = `Add ${name} to myFT`;
75
+ generatedProps['data-alternate-label'] = `Remove ${name} from myFT`;
76
+ if (alternateText) {
77
+ generatedProps['data-alternate-text'] = alternateText;
78
+ } else {
79
+ if (buttonText) {
80
+ generatedProps['data-alternate-text'] = buttonText;
81
+ } else {
82
+ generatedProps['data-alternate-text'] = 'Added';
83
+ }
84
+ }
85
+ }
86
+
87
+ if(variant) {
88
+ generatedProps[`n-myft-follow-button--${variant}`] = 'true';
89
+ }
90
+
91
+ if(followPlusDigestEmail) {
92
+ generatedProps['data-trackable-context-messaging'] = 'add-to-myft-plus-digest-button';
93
+ }
94
+
95
+ return generatedProps;
96
+ }
97
+
98
+ function getButtonText (props) {
99
+
100
+ const {
101
+ buttonText,
102
+ setFollowButtonStateToSelected,
103
+ cacheablePersonalisedUrl
104
+ } = props;
105
+ let outputText;
106
+
107
+ if(buttonText) {
108
+ outputText = buttonText;
109
+ } else {
110
+ if(setFollowButtonStateToSelected && cacheablePersonalisedUrl) {
111
+ outputText = 'Added';
112
+ } else {
113
+ outputText = 'Add to myFT';
114
+ }
115
+ }
116
+
117
+ return outputText;
118
+ }
119
+
120
+ /**
121
+ *
122
+ * @param {Object} props
123
+ * @param {string} props.name
124
+ * @param {Object} props.flags
125
+ * @param {string} props.extraClasses
126
+ * @param {string} props.conceptId
127
+ * @param {string} props.variant
128
+ * @param {string} props.buttonText
129
+ * @param {*} props.setFollowButtonStateToSelected
130
+ * @param {string} props.cacheablePersonalisedUrl
131
+ * @param {string} props.alternateText
132
+ * @param {*} props.followPlusDigestEmail
133
+ * @param {string} props.collectionName
134
+ */
135
+ export default function FollowButton (props) {
136
+
137
+ const {
138
+ name,
139
+ flags,
140
+ extraClasses,
141
+ conceptId,
142
+ variant,
143
+ } = props;
144
+
145
+ const formProps = generateFormProps(props);
146
+ const buttonProps = generateButtonProps(props);
147
+
148
+ const getVariantClass = (variant) => variant ? `n-myft-follow-button--${variant}` : '';
149
+
150
+ return (
151
+ <>
152
+ {flags.myFtApiWrite && <form
153
+ className={`n-myft-ui n-myft-ui--follow ${extraClasses || ''}`}
154
+ method="GET"
155
+ data-myft-ui="follow"
156
+ data-concept-id={conceptId}
157
+ {...formProps}>
158
+ <CsrfToken cacheablePersonalisedUrl={props.cacheablePersonalisedUrl} csrfToken={props.csrfToken} />
159
+ <div
160
+ className="n-myft-ui__announcement o-normalise-visually-hidden"
161
+ aria-live="assertive"
162
+ data-pressed-text={`Now following ${name}.`}
163
+ data-unpressed-text={`No longer following ${name}.`}
164
+ ></div>
165
+ <button
166
+ {...buttonProps}
167
+ className={[`n-myft-follow-button ${getVariantClass(variant)}`]}>
168
+ {getButtonText(props)}
169
+ </button>
170
+ </form>}
171
+ </>
172
+ );
173
+
174
+ }
@@ -5,7 +5,7 @@
5
5
  data-concept-id="{{conceptId}}"
6
6
  action="/myft/add/{{conceptId}}?instant=true"
7
7
  data-js-action="/__myft/api/core/followed/concept/{{conceptId}}?method=put">
8
- {{> n-myft-ui/components/csrf-token/input}}
8
+ {{{renderReactComponent localPath="components/csrf-token/input"}}}
9
9
  <input type="hidden" value="{{name}}" name="name">
10
10
  {{#if directType}}
11
11
  <input type="hidden" value="{{directType}}" name="directType">
@@ -2,7 +2,7 @@
2
2
  <span class="myft-pin-divider"></span>
3
3
  <div class="myft-pin-button-wrapper">
4
4
  <form method="post" action="/__myft/api/core/prioritised/concept/{{id}}?method={{#if prioritised}}delete{{else}}put{{/if}}" data-myft-prioritise>
5
- {{> n-myft-ui/components/csrf-token/input }}
5
+ {{{renderReactComponent localPath="components/csrf-token/input"}}}
6
6
  <input type="hidden" value="{{name}}" name="name"> {{#if directType}}
7
7
  <input type="hidden" value="{{directType}}" name="directType"> {{else}}
8
8
  <input type="hidden" value="http://www.ft.com/ontology/concept/Concept" name="directType"> {{/if}}
@@ -4,7 +4,7 @@
4
4
  data-myft-ui="saved"
5
5
  action="/myft/save/{{contentId}}"
6
6
  data-js-action="/__myft/api/core/saved/content/{{contentId}}?method=put">
7
- {{> n-myft-ui/components/csrf-token/input}}
7
+ {{{renderReactComponent localPath="components/csrf-token/input"}}}
8
8
  <div
9
9
  class="n-myft-ui__announcement o-normalise-visually-hidden"
10
10
  aria-live="assertive"
@@ -17,10 +17,14 @@
17
17
  data-trackable="{{#if trackableId}}{{trackableId}}{{else}}save-for-later{{/if}}"
18
18
  {{#if isSaved}}
19
19
  {{!-- The value of alternate label needs to be the opposite of label / the current saved state. This allows the client side JS to toggle the labels on state changes --}}
20
+ title="{{#if title}}{{title}} is{{/if}} Saved to myFT"
21
+ aria-label="{{#if title}}{{title}} is{{/if}} Saved to myFT"
20
22
  data-alternate-label="{{#if title}}Save {{title}} to myFT for later{{else}}Save this article to myFT for later{{/if}}"
21
23
  aria-pressed="true"
22
24
  {{else}}
23
- data-alternate-label="Saved to myFT"
25
+ title="{{#if title}}Save {{title}} to myFT for later{{else}}Save this article to myFT for later{{/if}}"
26
+ aria-label="{{#if title}}Save {{title}} to myFT for later{{else}}Save this article to myFT for later{{/if}}"
27
+ data-alternate-label="{{#if title}}{{title}} is{{/if}} Saved to myFT"
24
28
  aria-pressed="false"
25
29
  {{/if}}
26
30
  {{#unlessEquals appIsStreamPage true}}
@@ -40,13 +44,6 @@
40
44
  {{/if}}
41
45
  {{/if}}
42
46
  data-content-id="{{contentId}}" {{! duplicated here for tracking}}
43
- {{#if isSaved}}
44
- aria-label="{{#if title}}{{title}} is{{/if}} Saved to myFT"
45
- title="{{#if title}}{{title}} is{{/if}} Saved to myFT"
46
- {{else}}
47
- aria-label="{{#if title}}Save {{title}} to myFT for later{{else}}Save this article to myFT for later{{/if}}"
48
- title="{{#if title}}Save {{title}} to myFT for later{{else}}Save this article to myFT for later{{/if}}"
49
- {{/if}}
50
47
  >
51
48
  {{#if saveButtonWithIcon}}
52
49
  <span class="save-button-with-icon-copy" data-variant-label>{{#if buttonText~}}
@@ -1,65 +1,5 @@
1
1
  # n-myft-ui/unread-articles-indicator
2
2
 
3
- ![unread article indicator](https://user-images.githubusercontent.com/21194161/72087965-38d9c080-3301-11ea-9616-d1b31132c269.png)
3
+ Note: this component is decommissioned but a small function (`getNewArticlesSinceTime`) of it is still in use by next-myft-page to display a "New" label under an article in the Timeline view only.
4
4
 
5
-
6
- ## What?
7
-
8
- It indicates to the user that articles they've not yet read have been published to their myft feed since they last visited the site.
9
-
10
- ## Why?
11
-
12
- - To encourage a myFT feed habit
13
- - To provide a utility to the user so they don't need to check their feed unnecessarily
14
-
15
-
16
- ## Where?
17
-
18
- - **.com only**:
19
- - next-front-page
20
- - next-stream-page
21
- - next-article
22
- - next-myft-page
23
-
24
- ## When?
25
-
26
- - June 2018 - intial implementation [AB Test results](https://sites.google.com/ft.com/ftproductanalytics/analysis/myft/myft-feed-page)
27
- - Sep 2018 - sync across devices
28
- - Oct 2019 - add favicon [AB Test results](https://sites.google.com/ft.com/ftproductanalytics/tests/engagement/myftunreadfavicon)
29
- - Nov 2019 - automatically update (polling)
30
-
31
-
32
- ## Behaviour
33
-
34
- - The indicator consists of a red circle and a count of the number of new and unread articles since a user was last active on the site (or the app). The article count is hidden on the mobile breakpoint if the number is double digits (i.e. too large to fit in the circle)
35
-
36
- - The favicon is updated to reflect what the indicator is showing
37
-
38
- - The article count updates automatically without the user having to refresh the page every 5 minutes
39
-
40
- - The count is reset to 0 when the user visits their myFT feed page on [ft.com](https://www.ft.com/ft.com/myft/following)
41
-
42
- - The count is reset every day at midnight
43
-
44
- - The count is reset after 30 minutes of site inactivity - 🐛 thought to be a bug
45
-
46
- - The state of the indicator is synched between different browsers, however it's not expected to be real-time
47
- ---
48
-
49
- ## How the unread articles number is determined
50
-
51
- :one: Determine the time(`feedStartTime`) which is the user's last active time (the process is explained below)
52
-
53
- :two: Determine the time(`startTime`) to be used to count unread articles for the current visit. It is `feedStartTime` or `myFTIndicatorDismissedAt`&ast;, whichever comes later.
54
-
55
- :three: Fetch myft feed articles for the user
56
-
57
- :four: Count the articles published after the timestamp(`startTime`)
58
-
59
- ---
60
- &ast; `myFTIndicatorDismissedAt` timestamp is stored in `window.localStorage` when user visits [myFT feed page](https://www.ft.com/myft/following).
61
-
62
- ---
63
- ![unread article count](https://user-images.githubusercontent.com/21194161/72608180-11df4800-391a-11ea-973b-4a52933561ab.png)
64
-
65
- To keep the number synched on **cross devices**, it fetches the last 'active' time for a user via a Volt Procedure called UserInfo.
5
+ All other previous functionality of the component has been removed.
@@ -1,5 +1,5 @@
1
1
  // date-fns from v2 doesn't accept String arguments anymore.
2
- // the detail => https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md#200---2019-08-20
2
+ // the detail => https://github.com/date-fns/date-fns/blob/HEAD/CHANGELOG.md#200---2019-08-20
3
3
  // By adding validation for dates before their functions allows us to know it when unexpected value passed.
4
4
 
5
5
  import isTodayOriginal from 'date-fns/src/isToday';
@@ -11,22 +11,15 @@ import parseISO from 'date-fns/src/parseISO';
11
11
 
12
12
  const isValid = (date) => {
13
13
  if (!isValidOriginal(date)) {
14
- console.error('Invalid date passed', [date]); //eslint-disable-line
14
+ console.error("Invalid date passed", [date]); //eslint-disable-line
15
15
  }
16
16
  return date;
17
17
  };
18
18
 
19
19
  const isToday = (date) => isTodayOriginal(isValid(date));
20
- const isAfter = (date, dateToCompare) => isAfterOriginal(isValid(date), isValid(dateToCompare));
20
+ const isAfter = (date, dateToCompare) =>
21
+ isAfterOriginal(isValid(date), isValid(dateToCompare));
21
22
  const addMinutes = (date, amount) => addMinutesOriginal(isValid(date), amount);
22
23
  const startOfDay = (date) => startOfDayOriginal(isValid(date));
23
24
 
24
-
25
- export {
26
- isToday,
27
- isAfter,
28
- addMinutes,
29
- startOfDay,
30
- isValid,
31
- parseISO,
32
- };
25
+ export { isToday, isAfter, addMinutes, startOfDay, isValid, parseISO };
@@ -1,42 +1,11 @@
1
1
  import {startOfDay} from './date-fns';
2
2
  import * as storage from './storage';
3
- import * as ui from './ui';
4
- import updateCount from './update-count';
5
3
  import initialiseFeedStartTime from './initialise-feed-start-time';
6
4
  import sessionClient from 'next-session-client';
7
- import {UPDATE_INTERVAL} from './constants';
8
5
 
9
- let shouldPoll;
10
- let updateTimeout;
11
6
  let initialFeedStartTime;
12
7
  let userId;
13
8
 
14
- const isMyftFeedPage = window.location.pathname.indexOf('/myft/following') === 0;
15
- const doUpdate = () => updater().catch(stopPolling);
16
-
17
- export default async (options = {}) => {
18
- try {
19
- if (!storage.isAvailable()) return;
20
-
21
- const myftHeaderLink = document.querySelectorAll('.o-header__top-link--myft');
22
- const uiOpts = Object.assign({onClick: setDismissed, flags: {}}, options);
23
- shouldPoll = uiOpts.flags.MyFT_UnreadArticlesIndicatorPolling;
24
-
25
- await getNewArticlesSinceTime();
26
-
27
- const {count = 0} = storage.getLastUpdate() || {};
28
- ui.createIndicators(myftHeaderLink, uiOpts);
29
- ui.setCount(count);
30
-
31
- document.addEventListener('visibilitychange', onVisibilityChange);
32
- storage.addCountChangeListeners(newCount => ui.setCount(newCount));
33
- if (isMyftFeedPage) setDismissed();
34
- return updater();
35
- } catch(e) {
36
-
37
- }
38
- };
39
-
40
9
  async function getValidSession () {
41
10
  if (!userId) {
42
11
  const {uuid} = await sessionClient.uuid();
@@ -47,6 +16,7 @@ async function getValidSession () {
47
16
  }
48
17
 
49
18
  // Export used in next-myft -page to determine whether to add "New" label to articles in feed
19
+ //KEEP: This function is in use in next-myft-page do not delete!
50
20
  export async function getNewArticlesSinceTime () {
51
21
  const user = await getValidSession();
52
22
  const dayStart = startOfDay(new Date());
@@ -59,34 +29,3 @@ export async function getNewArticlesSinceTime () {
59
29
 
60
30
  return initialFeedStartTime || dayStart;
61
31
  }
62
-
63
- async function updater () {
64
- const user = await getValidSession();
65
- await updateCount(user, new Date());
66
- if (!shouldPoll) return;
67
- updateTimeout = window.setTimeout(doUpdate, UPDATE_INTERVAL);
68
- }
69
-
70
- async function onVisibilityChange () {
71
- if (document.visibilityState !== 'visible') return;
72
- try {
73
- await getValidSession();
74
- await getNewArticlesSinceTime();
75
- if (updateTimeout) window.clearTimeout(updateTimeout);
76
- await updater();
77
- } catch(e) {
78
- stopPolling();
79
- }
80
- }
81
-
82
- function stopPolling () {
83
- userId = undefined;
84
- if (updateTimeout) {
85
- window.clearTimeout(updateTimeout);
86
- }
87
- }
88
-
89
- function setDismissed () {
90
- storage.updateLastUpdate({count: 0, time: new Date()});
91
- storage.setIndicatorDismissedTime(new Date());
92
- }
@@ -2,10 +2,7 @@ import {isValid} from './date-fns';
2
2
 
3
3
  const DEVICE_SESSION_EXPIRY = 'deviceSessionExpiry';
4
4
  const FEED_START_TIME = 'newArticlesSinceTime';
5
- const LAST_INDICATOR_UPDATE = 'myFTIndicatorUpdate';
6
- const INDICATOR_DISMISSED_TIME = 'myFTIndicatorDismissedAt';
7
5
 
8
- const countChangeListeners = new Set();
9
6
  const isISOString = str => typeof str === 'string' && str.charAt(10) === 'T';
10
7
  const getStoredDate = key => {
11
8
  const value = window.localStorage.getItem(key);
@@ -13,7 +10,6 @@ const getStoredDate = key => {
13
10
 
14
11
  return isISOString(value) && isValid(date) ? date : null;
15
12
  };
16
- let lastCount;
17
13
 
18
14
  export const getDeviceSessionExpiry = () => getStoredDate(DEVICE_SESSION_EXPIRY);
19
15
 
@@ -23,10 +19,6 @@ export const getFeedStartTime = () => getStoredDate(FEED_START_TIME);
23
19
 
24
20
  export const setFeedStartTime = date => window.localStorage.setItem(FEED_START_TIME, date.toISOString());
25
21
 
26
- export const getIndicatorDismissedTime = () => getStoredDate(INDICATOR_DISMISSED_TIME);
27
-
28
- export const setIndicatorDismissedTime = date => window.localStorage.setItem(INDICATOR_DISMISSED_TIME, date.toISOString());
29
-
30
22
  export const isAvailable = () => {
31
23
  try {
32
24
  const storage = window.localStorage;
@@ -39,39 +31,3 @@ export const isAvailable = () => {
39
31
  return false;
40
32
  }
41
33
  };
42
-
43
-
44
- export const setLastUpdate = (update = {}) => {
45
- const toStore = Object.assign({}, update);
46
- if (update.time) toStore.time = update.time.toISOString();
47
- if (update.updateStarted) toStore.updateStarted = update.updateStarted.toISOString();
48
- window.localStorage.setItem(LAST_INDICATOR_UPDATE, JSON.stringify(toStore));
49
- fireListeners();
50
- };
51
-
52
- export const getLastUpdate = () => {
53
- try {
54
- const lastUpdate = JSON.parse(window.localStorage.getItem(LAST_INDICATOR_UPDATE));
55
- if (lastUpdate.time) lastUpdate.time = new Date(lastUpdate.time);
56
- if (lastUpdate.updateStarted) lastUpdate.updateStarted = new Date(lastUpdate.updateStarted);
57
- return lastUpdate;
58
- } catch (e) {}
59
- };
60
-
61
- export const updateLastUpdate = (update) => setLastUpdate(Object.assign({}, getLastUpdate(), update) );
62
-
63
- export const addCountChangeListeners = listener => {
64
- if (!countChangeListeners.size) {
65
- window.addEventListener('storage', fireListeners);
66
- }
67
- countChangeListeners.add(listener);
68
- };
69
-
70
- function fireListeners () {
71
- const {count} = getLastUpdate() || {};
72
- if (count === lastCount) return;
73
- for (const listener of countChangeListeners) {
74
- listener(count || 0);
75
- }
76
- lastCount = count || 0;
77
- }
package/demos/app.js CHANGED
@@ -1,9 +1,17 @@
1
+ require('sucrase/register');
1
2
  const express = require('@financial-times/n-internal-tool');
2
3
  const chalk = require('chalk');
3
4
  const errorHighlight = chalk.bold.red;
4
5
  const highlight = chalk.bold.green;
5
-
6
+ const { PageKitReactJSX } = require('@financial-times/dotcom-server-react-jsx');
7
+ const fs = require('fs');
8
+ const path = require('path');
6
9
  const xHandlebars = require('@financial-times/x-handlebars');
10
+ const handlebars = require('handlebars');
11
+ const { helpers } = require('@financial-times/dotcom-server-handlebars');
12
+
13
+ const demoJSX = require('./templates/demo').default;
14
+ const demoLayoutSource = fs.readFileSync(path.join(__dirname, './templates/demo-layout.html'),'utf8').toString();
7
15
 
8
16
  const fixtures = {
9
17
  followButton: require('./fixtures/follow-button'),
@@ -29,10 +37,13 @@ const app = module.exports = express({
29
37
  demo: true,
30
38
  s3o: false,
31
39
  helpers: {
32
- x: xHandlebars()
33
- }
40
+ x: xHandlebars(),
41
+ renderReactComponent: helpers.renderReactComponent
42
+ },
34
43
  });
35
44
 
45
+ const jsxRenderer = (new PageKitReactJSX({ includeDoctype: false }));
46
+
36
47
  app.get('/', (req, res) => {
37
48
  res.render('demo', Object.assign({
38
49
  title: 'n-myft-ui demo',
@@ -44,6 +55,22 @@ app.get('/', (req, res) => {
44
55
  }, fixtures));
45
56
  });
46
57
 
58
+ app.get('/demo-jsx', async (req, res) => {
59
+ let demo = await jsxRenderer.render(demoJSX, Object.assign({
60
+ title: 'n-myft-ui demo',
61
+ layout: 'demo-layout',
62
+ flags: {
63
+ myFtApi: true,
64
+ myFtApiWrite: true
65
+ }
66
+ }, fixtures));
67
+
68
+ let template = handlebars.compile(demoLayoutSource);
69
+ let result = template({body: demo});
70
+
71
+ res.send(result);
72
+ });
73
+
47
74
  app.get('/digest-on-follow', (req, res) => {
48
75
  res.render('digest-on-follow', Object.assign({
49
76
  title: 'n-myft-ui digest on follow',