@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
@@ -12,7 +12,7 @@
12
12
  </h2>
13
13
 
14
14
  {{#followButton}}
15
- {{> n-myft-ui/components/follow-button/follow-button}}
15
+ {{{renderReactComponent localPath="components/follow-button/follow-button" flags=@root.flags}}}
16
16
  {{/followButton}}
17
17
 
18
18
  <h2
@@ -21,9 +21,7 @@
21
21
  </h2>
22
22
 
23
23
  {{#followButton}}
24
- {{> n-myft-ui/components/follow-button/follow-button
25
- buttonText=name
26
- }}
24
+ {{{renderReactComponent localPath="components/follow-button/follow-button" buttonText=name flags=@root.flags}}}
27
25
  {{/followButton}}
28
26
 
29
27
  {{#saveButton}}
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import FollowButton from '../../components/follow-button/follow-button';
3
+
4
+ export default function Demo (props) {
5
+
6
+ const {
7
+ title,
8
+ flags,
9
+ followButton,
10
+ } = props;
11
+
12
+ const followButtonProps = {...followButton, flags};
13
+
14
+ return (
15
+ <div className="o-grid-container o-grid-container--snappy demo-container">
16
+ <h1>{title}</h1>
17
+
18
+ <section
19
+ id="follow-button"
20
+ className="demo-section">
21
+ <div className="o-grid-row">
22
+ <div data-o-grid-colspan="12">
23
+ <h2
24
+ className="demo-section__title">
25
+ Follow button
26
+ </h2>
27
+ <FollowButton {...followButtonProps} />
28
+ </div>
29
+ </div>
30
+ </section>
31
+ </div>
32
+ )
33
+ }
@@ -5,7 +5,7 @@
5
5
  <h2>Follow button</h2>
6
6
 
7
7
  {{#followButton}}
8
- {{> n-myft-ui/components/follow-button/follow-button}}
8
+ {{{renderReactComponent localPath="components/follow-button/follow-button" flags=@root.flags}}}
9
9
  {{/followButton}}
10
10
  </div>
11
11
  </div>
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ transform: {
3
+ '.(js|jsx)': '@sucrase/jest-plugin',
4
+ },
5
+ testEnvironment: 'jest-environment-jsdom',
6
+ modulePathIgnorePatterns: ['node_modules', 'bower_components'],
7
+ testMatch: ['<rootDir>/components/**/*.test.js']
8
+ };
@@ -4,28 +4,32 @@ $myft-lozenge-themes: (
4
4
  text: oColorsByName('white'),
5
5
  highlight: oColorsByName('claret-50'),
6
6
  pressed-highlight: rgba(oColorsByName('black'), 0.05),
7
- disabled: rgba(oColorsByName('black'), 0.5)
7
+ disabled: rgba(oColorsByName('black'), 0.5),
8
+ focus-outline: oColorsByUsecase('focus', 'outline', $fallback: null)
8
9
  ),
9
10
  inverse: (
10
11
  background: oColorsByName('white'),
11
12
  text: oColorsByName('claret'),
12
13
  highlight: rgba(white, 0.8),
13
14
  pressed-highlight: rgba(white, 0.2),
14
- disabled: rgba(oColorsByName('white'), 0.5)
15
+ disabled: rgba(oColorsByName('white'), 0.5),
16
+ focus-outline: oColorsByName('white')
15
17
  ),
16
18
  opinion: (
17
19
  background: oColorsByName('oxford-40'),
18
20
  text: oColorsByName('white'),
19
21
  highlight: oColorsByName('oxford-30'),
20
22
  pressed-highlight: rgba(oColorsByName('oxford-40'), 0.2),
21
- disabled: rgba(oColorsByName('black'), 0.5)
23
+ disabled: rgba(oColorsByName('black'), 0.5),
24
+ focus-outline: oColorsByUsecase('focus', 'outline', $fallback: null)
22
25
  ),
23
26
  monochrome: (
24
27
  background: oColorsByName('white'),
25
28
  text: oColorsByName('black'),
26
29
  highlight: oColorsByName('white-80'),
27
30
  pressed-highlight: rgba(oColorsByName('white'), 0.2),
28
- disabled: rgba(oColorsByName('white'), 0.5)
31
+ disabled: rgba(oColorsByName('white'), 0.5),
32
+ focus-outline: oColorsByName('white')
29
33
  )
30
34
  );
31
35
 
@@ -1,6 +1,54 @@
1
1
  @import './themes';
2
2
  @import './toggle-icon';
3
3
 
4
+ @mixin focusOutlineColor($focus-color) {
5
+ // Apply :focus styles as a fallback
6
+ // These styles will be applied to all browsers that don't use the polyfill, this includes browsers which support the feature natively.
7
+ body:not(.js-focus-visible) &,
8
+ html:not(.js-focus-visible) & {
9
+ // Standardise focus styles.
10
+ &:focus {
11
+ outline: 2px solid $focus-color;
12
+ }
13
+ }
14
+
15
+ // When the focus-visible polyfill is applied `.js-focus-visible` is added to the html dom node
16
+ // (the body node in v4 of the 3rd party polyfill)
17
+
18
+ // stylelint-disable-next-line selector-no-qualifying-type
19
+ body.js-focus-visible &, // stylelint-disable-next-line selector-no-qualifying-type
20
+ html.js-focus-visible & {
21
+ // Standardise focus styles.
22
+ // stylelint-disable-next-line selector-no-qualifying-type
23
+ &.focus-visible {
24
+ outline: 2px solid $focus-color;
25
+ }
26
+ // Disable browser default focus style.
27
+ // stylelint-disable-next-line selector-no-qualifying-type
28
+ &:focus:not(.focus-visible) {
29
+ outline: 0;
30
+ }
31
+ }
32
+
33
+ // These styles will be ignored by browsers which do not recognise the :focus-visible selector (as per the third bullet point in https://www.w3.org/TR/selectors-3/#Conformance)
34
+ // If a browser supports :focus-visible we unset the :focus styles that were applied above
35
+ // (within the html:not(.js-focus-visible) block).
36
+ &:focus-visible,
37
+ body:not(.js-focus-visible) &:focus,
38
+ html:not(.js-focus-visible) &:focus {
39
+ outline: unset;
40
+ }
41
+
42
+ // Styles given :focus-visible support. Extra selectors needed to match
43
+ // previous `:focus` selector specificity.
44
+ body:not(.js-focus-visible) &:focus-visible,
45
+ html:not(.js-focus-visible) &:focus-visible,
46
+ &:focus-visible {
47
+ outline: 2px solid $focus-color;
48
+ }
49
+ }
50
+
51
+
4
52
  @mixin myftLozengeTheme($theme: standard, $with-toggle-icon: false) {
5
53
  @if $with-toggle-icon != false {
6
54
  @include myftToggleIcon($theme);
@@ -11,6 +59,8 @@
11
59
  border: 1px solid getThemeColor(background);
12
60
  color: getThemeColor(background);
13
61
 
62
+ @include focusOutlineColor(getThemeColor(focus-outline));
63
+
14
64
  &:hover,
15
65
  &:focus {
16
66
  background-color: getThemeColor(pressed-highlight);
@@ -57,8 +107,4 @@
57
107
  text-overflow: ellipsis;
58
108
  transition: border-color, background-color 0.5s ease;
59
109
  white-space: nowrap;
60
-
61
- &:focus {
62
- outline: 2px solid oColorsByName('teal-90');
63
- }
64
110
  }
package/myft/main.scss CHANGED
@@ -13,7 +13,6 @@ $n-notification-is-silent: false !default;
13
13
 
14
14
  @import './ui/myft-buttons/main';
15
15
  @import './ui/lists';
16
- @import '../components/header-tooltip/main';
17
16
  @import '../components/pin-button/main';
18
17
  @import '../components/instant-alert/main';
19
18
 
package/myft/ui/index.js CHANGED
@@ -2,12 +2,10 @@ import * as myFtButtons from './myft-buttons';
2
2
  import * as lists from './lists';
3
3
  import personaliseLinks from './personalise-links';
4
4
  import updateUi from './update-ui';
5
- import * as headerTooltip from '../../components/header-tooltip';
6
5
 
7
6
  function init (opts) {
8
7
  myFtButtons.init(opts);
9
8
  lists.init();
10
- headerTooltip.init();
11
9
  }
12
10
 
13
11
  export {
package/package.json CHANGED
@@ -1,14 +1,11 @@
1
1
  {
2
2
  "name": "@financial-times/n-myft-ui",
3
- "version": "23.0.1",
3
+ "version": "24.0.0",
4
4
  "description": "Client side component for interaction with myft",
5
5
  "main": "server.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1",
8
8
  "commit": "commit-wizard",
9
- "precommit": "node_modules/.bin/secret-squirrel",
10
- "prepush": "make verify -j3",
11
- "commitmsg": "node_modules/.bin/secret-squirrel-commitmsg",
12
9
  "prepare": "npx snyk protect || npx snyk protect -d || true"
13
10
  },
14
11
  "repository": {
@@ -27,10 +24,15 @@
27
24
  "@financial-times/dotcom-build-js": "0.4.1",
28
25
  "@financial-times/dotcom-build-sass": "0.4.1",
29
26
  "@financial-times/dotcom-page-kit-cli": "0.4.1",
30
- "@financial-times/n-gage": "^3.12.0",
27
+ "@financial-times/dotcom-server-handlebars": "^3.0.0",
28
+ "@financial-times/dotcom-server-react-jsx": "^2.6.2",
29
+ "@financial-times/n-gage": "^8.3.0",
31
30
  "@financial-times/n-heroku-tools": "8.3.1",
32
31
  "@financial-times/n-internal-tool": "2.3.4",
33
32
  "@financial-times/x-handlebars": "1.0.0-beta.21",
33
+ "@sucrase/jest-plugin": "^2.2.0",
34
+ "@testing-library/jest-dom": "^5.16.1",
35
+ "@testing-library/react": "^12.1.2",
34
36
  "ascii-table": "0.0.9",
35
37
  "autoprefixer": "9.7.0",
36
38
  "aws-sdk-mock": "4.5.0",
@@ -53,6 +55,7 @@
53
55
  "css-loader": "^0.23.1",
54
56
  "denodeify": "^1.2.1",
55
57
  "eslint": "6.5.1",
58
+ "eslint-plugin-react": "^7.27.1",
56
59
  "extract-css-block-webpack-plugin": "^1.3.0",
57
60
  "extract-text-webpack-plugin": "3.0.2",
58
61
  "fetch-mock": "^5.0.3",
@@ -62,6 +65,8 @@
62
65
  "hyperons": "^0.4.1",
63
66
  "imports-loader": "0.8.0",
64
67
  "inject-loader": "^3.0.0",
68
+ "jest": "^27.4.5",
69
+ "jsdom": "^19.0.0",
65
70
  "karma": "4.4.1",
66
71
  "karma-browserstack-launcher": "1.5.1",
67
72
  "karma-chai": "^0.1.0",
@@ -83,16 +88,25 @@
83
88
  "npm-prepublish": "^1.2.1",
84
89
  "pa11y-ci": "^2.1.1",
85
90
  "postcss-loader": "^0.9.1",
91
+ "react": "^17.0.2",
86
92
  "regenerator-runtime": "^0.13.3",
87
93
  "sass-loader": "^3.2.0",
88
94
  "semver": "6.3.0",
89
95
  "sinon": "^7.1.0",
90
96
  "sinon-chai": "^3.2.0",
91
- "snyk": "^1.216.5"
97
+ "snyk": "^1.216.5",
98
+ "sucrase": "^3.10.1"
92
99
  },
93
100
  "x-dash": {
94
101
  "engine": {
95
102
  "server": "hyperons"
96
103
  }
104
+ },
105
+ "husky": {
106
+ "hooks": {
107
+ "commit-msg": "node_modules/.bin/secret-squirrel-commitmsg",
108
+ "pre-commit": "node_modules/.bin/secret-squirrel",
109
+ "pre-push": "make verify -j3"
110
+ }
97
111
  }
98
112
  }
@@ -1,95 +1,43 @@
1
1
  /* global expect */
2
2
  import sinon from 'sinon';
3
3
 
4
- const NEW_ARTICLES = [
5
- { id: 'article-1' },
6
- { id: 'article-2' },
7
- { id: 'article-3' },
8
- ];
9
4
  const FEED_START_TIME = '2018-06-05T10:00:00.000Z';
10
5
 
11
6
  describe('unread stories indicator', () => {
12
- let unreadStoriesIndicator;
13
- let mockUpdate;
7
+ let unreadArticlesComponent;
14
8
  let mockInitialiseFeedStartTime;
15
9
  let mockStorage;
16
- let mockUi;
17
10
  let isStorageAvailable;
18
11
 
19
12
  beforeEach(() => {
20
13
  mockStorage = {
21
14
  getFeedStartTime: sinon.stub().returns(FEED_START_TIME),
22
15
  setFeedStartTime: sinon.stub(),
23
- getLastUpdate: sinon.stub().returns({count: 22}),
24
- updateLastUpdate: sinon.stub(),
25
- isAvailable: sinon.stub().callsFake(() => isStorageAvailable),
26
- setIndicatorDismissedTime: sinon.stub(),
27
- addCountChangeListeners: sinon.stub()
16
+ isAvailable: sinon.stub().callsFake(() => isStorageAvailable)
28
17
  };
29
- mockUi = {
30
- createIndicators: sinon.stub(),
31
- setCount: sinon.stub(),
32
- getState: sinon.stub(),
33
- };
34
- mockUpdate = sinon.stub().returns(Promise.resolve(NEW_ARTICLES));
18
+
35
19
  mockInitialiseFeedStartTime = sinon.stub().resolves();
36
20
 
37
- unreadStoriesIndicator = require('inject-loader!../../components/unread-articles-indicator')({
21
+ unreadArticlesComponent = require('inject-loader!../../components/unread-articles-indicator')({
38
22
  'next-session-client': {
39
23
  uuid: sinon.stub().resolves({uuid: '00000000-0000-0000-0000-000000000000'})
40
24
  },
41
25
  './storage': mockStorage,
42
- './ui': mockUi,
43
- './update-count': mockUpdate,
44
26
  './initialise-feed-start-time': mockInitialiseFeedStartTime
45
27
  });
46
28
  });
47
29
 
48
- describe('default export', () => {
49
- describe('storage availability', () => {
50
- it('should not do anything if storage is not available', () => {
51
- isStorageAvailable = false;
52
- unreadStoriesIndicator.default();
53
- expect(mockUi.createIndicators).to.not.have.been.called;
54
- expect(mockUi.setCount).to.not.have.been.called;
55
- expect(mockInitialiseFeedStartTime).to.not.have.been.called;
56
- });
30
+ describe('getNewsArticlesSinceTime', () => {
31
+ it('should not do anything if storage is not available', () => {
32
+ isStorageAvailable = false;
33
+ unreadArticlesComponent.getNewArticlesSinceTime();
34
+ expect(mockInitialiseFeedStartTime).to.not.have.been.called;
57
35
  });
58
36
 
59
- describe('initialisation', () => {
60
- beforeEach(() => {
61
- isStorageAvailable = true;
62
- return unreadStoriesIndicator.default();
63
- });
64
-
65
- it('should initialise feed start time', () => {
66
- expect(mockInitialiseFeedStartTime).to.have.been.calledOnce;
67
- });
68
-
69
- it('should create ui indicators', () => {
70
- expect(mockUi.createIndicators).to.have.been.calledOnce;
71
- });
72
-
73
- it('should set the ui indicators initial count', () => {
74
- expect(mockUi.setCount).to.have.been.calledOnce;
75
- expect(mockUi.setCount).to.have.been.calledWith(22);
76
- });
77
-
78
- it('should set a listener for storage changes', () => {
79
- expect(mockStorage.addCountChangeListeners).to.have.been.calledOnce;
80
- });
81
-
82
-
83
- it('should handle clicks', () => {
84
- const args = mockUi.createIndicators.firstCall.args;
85
-
86
- expect(args[1].onClick).to.be.a('function');
87
-
88
- args[1].onClick();
89
-
90
- expect(mockStorage.setIndicatorDismissedTime).to.have.been.calledOnce;
91
- expect(mockStorage.updateLastUpdate).to.have.been.calledOnce;
92
- });
37
+ it('should initialise feed start time', () => {
38
+ isStorageAvailable = true;
39
+ return unreadArticlesComponent.getNewArticlesSinceTime();
40
+ expect(mockInitialiseFeedStartTime).to.have.been.calledOnce;
93
41
  });
94
42
  });
95
43
  });
@@ -126,99 +126,6 @@ describe('storage', () => {
126
126
  });
127
127
  });
128
128
 
129
- describe('getLastUpdate', () => {
130
- beforeEach(() => {
131
- mockStorage.myFTIndicatorUpdate = JSON.stringify({foo:1, time: now.toISOString(), updateStarted: now.toISOString()});
132
- });
133
-
134
- it('converts time from ISOString to Date', () => {
135
- expect(storage.getLastUpdate().time).to.be.a('Date');
136
- });
137
-
138
- it('converts updateStarted from ISOString to Date', () => {
139
- expect(storage.getLastUpdate().updateStarted).to.be.a('Date');
140
- });
141
-
142
- it('gives the right time', () => {
143
- expect(storage.getLastUpdate().time.getTime()).to.be.equal(now.getTime());
144
- });
145
-
146
- it('gives the right updateStarted', () => {
147
- expect(storage.getLastUpdate().updateStarted.getTime()).to.be.equal(now.getTime());
148
- });
149
- });
150
-
151
- describe('getLastUpdate when updateStarted is false', () => {
152
- beforeEach(() => {
153
- mockStorage.myFTIndicatorUpdate = JSON.stringify({foo:1, time: now.toISOString(), updateStarted: false});
154
- });
155
-
156
- it('gives the right updateStarted', () => {
157
- expect(storage.getLastUpdate().updateStarted).equal(false);
158
- });
159
- });
160
-
161
- describe('updateLastUpdate', () => {
162
- describe('when updating time', () =>{
163
- const date = new Date(2018, 5, 1, 11, 30, 0);
164
- beforeEach(() => {
165
- mockStorage.myFTIndicatorUpdate = JSON.stringify({foo: 3, time: now.toISOString()});
166
- storage.updateLastUpdate({time: date});
167
- });
168
-
169
- it('converts time from Date to ISOString', () => {
170
- expect(JSON.parse(mockStorage.myFTIndicatorUpdate).time).to.equal(date.toISOString());
171
- });
172
-
173
- it('leaves other properties unchanged', () => {
174
- expect(JSON.parse(mockStorage.myFTIndicatorUpdate).foo).to.equal(3);
175
- });
176
- });
177
- describe('when updating updateStarted with a Date', () =>{
178
- const date = new Date(2018, 5, 1, 11, 30, 0);
179
- beforeEach(() => {
180
- mockStorage.myFTIndicatorUpdate = JSON.stringify({foo: 3, updateStarted: now.toISOString()});
181
- storage.updateLastUpdate({updateStarted: date});
182
- });
183
-
184
- it('converts updateStarted from Date to ISOString', () => {
185
- expect(JSON.parse(mockStorage.myFTIndicatorUpdate).updateStarted).to.equal(date.toISOString());
186
- });
187
-
188
- it('leaves other properties unchanged', () => {
189
- expect(JSON.parse(mockStorage.myFTIndicatorUpdate).foo).to.equal(3);
190
- });
191
- });
192
- describe('when updating updateStarted with false', () =>{
193
- beforeEach(() => {
194
- mockStorage.myFTIndicatorUpdate = JSON.stringify({foo: 3, updateStarted: now.toISOString()});
195
- storage.updateLastUpdate({updateStarted: false});
196
- });
197
-
198
- it('sets updateStarted to false', () => {
199
- expect(JSON.parse(mockStorage.myFTIndicatorUpdate).updateStarted).to.equal(false);
200
- });
201
- });
202
- describe('when updating other properties', () =>{
203
- beforeEach(() => {
204
- mockStorage.myFTIndicatorUpdate = JSON.stringify({foo: 3, bar:4, baz:5});
205
- storage.updateLastUpdate({bar: 5, baz:6});
206
- });
207
-
208
- it('updates existing properties', () => {
209
- expect(JSON.parse(mockStorage.myFTIndicatorUpdate).bar).to.equal(5);
210
- });
211
-
212
- it('add new properties', () => {
213
- expect(JSON.parse(mockStorage.myFTIndicatorUpdate).baz).to.equal(6);
214
- });
215
-
216
- it('leaves other properties unchanged', () => {
217
- expect(JSON.parse(mockStorage.myFTIndicatorUpdate).foo).to.equal(3);
218
- });
219
- });
220
- });
221
-
222
129
  describe('isAvailable', () => {
223
130
  it('should return true if it is available', () => {
224
131
  sinon.stub(window.Storage.prototype, 'removeItem').callsFake((key) => delete mockStorage[key]);
@@ -1,5 +0,0 @@
1
- <input
2
- data-myft-csrf-token
3
- value="{{#if @root.cacheablePersonalisedUrl}}{{@root.csrfToken}}{{/if}}"
4
- type="hidden"
5
- name="token">
@@ -1,79 +0,0 @@
1
- {{#if @root.flags.myFtApiWrite}}
2
- <form
3
- class="n-myft-ui n-myft-ui--follow {{extraClasses}}"
4
- method="GET"
5
- data-myft-ui="follow"
6
- data-concept-id="{{conceptId}}"
7
- {{#if collectionName}}data-myft-tracking="collectionName={{collectionName}}"{{/if}}
8
- {{#if followPlusDigestEmail}}
9
- action="/__myft/api/core/follow-plus-digest-email/{{conceptId}}?method=put"
10
- data-myft-ui-variant="followPlusDigestEmail"
11
- {{else}}
12
- {{#ifAll setFollowButtonStateToSelected @root.cacheablePersonalisedUrl}}
13
- action="/myft/remove/{{conceptId}}"
14
- data-js-action="/__myft/api/core/followed/concept/{{conceptId}}?method=delete"
15
- {{else}}
16
- action="/myft/add/{{conceptId}}"
17
- data-js-action="/__myft/api/core/followed/concept/{{conceptId}}?method=put"
18
- {{/ifAll}}
19
- {{/if}}>
20
- {{> n-myft-ui/components/csrf-token/input}}
21
- <div
22
- class="n-myft-ui__announcement o-normalise-visually-hidden"
23
- aria-live="assertive"
24
- data-pressed-text="Now following {{name}}."
25
- data-unpressed-text="No longer following {{name}}."
26
- ></div>
27
- <button
28
- {{#ifAll setFollowButtonStateToSelected @root.cacheablePersonalisedUrl}}
29
- aria-label="Remove {{name}} from myFT"
30
- title="Remove {{name}} from myFT"
31
- data-alternate-label="Add {{name}} to myFT"
32
- aria-pressed="true"
33
- {{#if alternateText}}
34
- data-alternate-text="{{alternateText}}"
35
- {{else}}
36
- {{#if buttonText}}
37
- data-alternate-text="{{buttonText}}"
38
- {{else}}
39
- data-alternate-text="Add to myFT"
40
- {{/if}}
41
- {{/if}}
42
- {{else}}
43
- aria-pressed="false"
44
- aria-label="Add {{name}} to myFT"
45
- title="Add {{name}} to myFT"
46
- data-alternate-label="Remove {{name}} from myFT"
47
- {{#if alternateText}}
48
- data-alternate-text="{{alternateText}}"
49
- {{else}}
50
- {{#if buttonText}}
51
- data-alternate-text="{{buttonText}}"
52
- {{else}}
53
- data-alternate-text="Added"
54
- {{/if}}
55
- {{/if}}
56
- {{/ifAll}}
57
- class="{{extraButtonClasses}}
58
- n-myft-follow-button
59
- {{~#variant}} n-myft-follow-button--{{this}}{{/variant~}}"
60
- data-concept-id="{{conceptId}}" {{! duplicated here for tracking}}
61
- {{#if followPlusDigestEmail}}
62
- data-trackable-context-messaging="add-to-myft-plus-digest-button"
63
- {{/if}}
64
- data-trackable="follow"
65
- type="submit">
66
- {{~#if buttonText~}}
67
- {{buttonText}}
68
- {{~else~}}
69
- {{~#ifAll setFollowButtonStateToSelected @root.cacheablePersonalisedUrl~}}
70
- Added
71
- {{~else~}}
72
- Add to myFT
73
- {{~/ifAll~}}
74
- {{~/if~}}
75
- </button>
76
- </form>
77
- {{else}}
78
- <!-- Add to myFT button hidden due to myFtApiWrite being off -->
79
- {{/if}}
@@ -1,37 +0,0 @@
1
- import Tooltip from 'o-tooltip';
2
- import myftClient from 'next-myft-client';
3
-
4
- const isMyftPage = window.location.pathname.startsWith('/myft');
5
- const externalReferrer = !document.referrer || !(new URL(document.referrer).hostname.endsWith('ft.com'));
6
- const myftHeaderLogo = document.querySelector('.o-header__top-link--myft');
7
- const flagIsEnabled = window.FT && window.FT.flags && window.FT.flags.get('myFT_HeaderTooltip');
8
-
9
- export const init = () => {
10
- if (!externalReferrer || isMyftPage || !myftHeaderLogo || !flagIsEnabled) {
11
- return;
12
- }
13
-
14
- return myftClient.getAll('followed', 'concept')
15
- .then(followedConcepts => {
16
- if (followedConcepts.length) {
17
- const concepts = followedConcepts
18
- .sort((a, b) => b.lastPublished - a.lastPublished)
19
- .map(({name}) => `<span style="no-wrap">${name}</span>`).slice(0, 3);
20
- let content = 'Read the latest on ';
21
-
22
- if (concepts.length === 3) {
23
- content += `${concepts.shift()}, `;
24
- }
25
-
26
- content += concepts.join(' and ');
27
- content += ' stories.';
28
-
29
- new Tooltip(myftHeaderLogo, {
30
- target: 'myft-header-tooltip',
31
- content: content,
32
- showOnConstruction: true,
33
- position: 'below'
34
- });
35
- }
36
- });
37
- };
@@ -1,12 +0,0 @@
1
- .o-header__top-link--myft__container {
2
- position: relative;
3
-
4
- .o-tooltip {
5
- white-space: normal;
6
- text-align: left;
7
- min-width: 200px;
8
- @include oGridRespondTo('L') {
9
- min-width: 250px;
10
- }
11
- }
12
- }
@@ -1,4 +0,0 @@
1
- const MINUTE = 60 * 1000;
2
-
3
- export const UPDATE_INTERVAL = 5 * MINUTE; // how often we should refresh the data from the server
4
- export const UPDATE_TIMEOUT = 10 * MINUTE; // how long before we assume an update finished without tidying up.
@@ -1,26 +0,0 @@
1
- import {json as fetchJson} from 'fetchres';
2
- import {isAfter, parseISO} from './date-fns';
3
-
4
- export default async (userId, startTime) => {
5
- const articles = await fetchContentFromPersonalisedFeed(userId);
6
- const unreadArticlesSinceLastVisit = articles
7
- .filter(({userCompletion = -1}) => userCompletion < 1) // only include unread articles
8
- .filter(({contentTimeStamp}) => isAfter(parseISO(contentTimeStamp), startTime)); // only include articles published since last visit
9
-
10
- return unreadArticlesSinceLastVisit.length;
11
- };
12
-
13
- async function fetchContentFromPersonalisedFeed (userId) {
14
-
15
- const searchParams = new URLSearchParams({
16
- originatingSignals: 'followed',
17
- from: '-1d',
18
- source: 'myft-ui'
19
- });
20
-
21
- const res = await fetch(`/__myft/api/onsite/feed/${userId}?${searchParams.toString()}`, {
22
- credentials: 'include'
23
- });
24
- const {results = []} = await fetchJson(res);
25
- return results;
26
- }