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

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 (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
- }