@financial-times/n-myft-ui 30.3.0 → 30.4.2
Sign up to get free protection for your applications and to get access to all the features.
- package/.babelrc +9 -0
- package/.circleci/config.yml +3 -0
- package/.eslintignore +1 -0
- package/CODEOWNERS +1 -0
- package/Makefile +3 -0
- package/build-state/npm-shrinkwrap.json +17081 -8036
- package/components/jsx/csrf-token/input.jsx +28 -0
- package/components/jsx/follow-plus-instant-alerts/follow-plus-instant-alerts.jsx +95 -0
- package/components/jsx/follow-plus-instant-alerts/index.js +76 -0
- package/components/jsx/follow-plus-instant-alerts/main.scss +64 -0
- package/components/jsx/preferences-modal/index.js +182 -0
- package/components/jsx/preferences-modal/main.scss +59 -0
- package/components/jsx/preferences-modal/preferences-modal.jsx +53 -0
- package/demos/app.js +5 -1
- package/demos/fixtures/jsx/follow-plus-instant-alerts.json +4 -0
- package/demos/fixtures/jsx/preferences-modal.json +3 -0
- package/demos/src/demo.scss +2 -0
- package/demos/templates/demo.html +78 -28
- package/myft/main.scss +3 -0
- package/myft/ui/myft-buttons/do-form-submit.js +9 -2
- package/package.json +7 -2
@@ -0,0 +1,28 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* @typedef {object} CsrfInputProperties
|
5
|
+
* @property {string} [csrfToken]
|
6
|
+
* A token to mitigate Cross Site Request Forgery
|
7
|
+
* @property {boolean} [cacheablePersonalisedUrl]
|
8
|
+
* An indicator to decide whether its safe to set the button state on the server side. eg. there is no cache or the cache is personalised
|
9
|
+
*/
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Create a follow plus instant alerts button component
|
13
|
+
* @public
|
14
|
+
* @param {CsrfInputProperties}
|
15
|
+
* @returns {React.ReactElement}
|
16
|
+
*/
|
17
|
+
|
18
|
+
export default function CsrfToken({ csrfToken, cacheablePersonalisedUrl }) {
|
19
|
+
|
20
|
+
const token = cacheablePersonalisedUrl ? csrfToken : '';
|
21
|
+
return (
|
22
|
+
<input
|
23
|
+
data-myft-csrf-token
|
24
|
+
value={token}
|
25
|
+
type="hidden"
|
26
|
+
name="token" />
|
27
|
+
)
|
28
|
+
}
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import CsrfToken from '../csrf-token/input';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* @typedef {object} FollowProperties
|
6
|
+
* @property {string} conceptId
|
7
|
+
* The ID for the concept
|
8
|
+
* @property {string} name
|
9
|
+
* The user facing label for the concept
|
10
|
+
* @property {string} [csrfToken]
|
11
|
+
* A token to mitigate Cross Site Request Forgery
|
12
|
+
* @property {boolean} [setFollowButtonStateToSelected]
|
13
|
+
* An indicator to state whether the button state should be set to selected on the server side
|
14
|
+
* @property {boolean} [cacheablePersonalisedUrl]
|
15
|
+
* An indicator to decide whether its safe to set the button state on the server side. eg. there is no cache or the cache is personalised
|
16
|
+
* @property {boolean} [setInstantAlertsOn]
|
17
|
+
* An indicator to switch the rendering to show instant alerts as turned on
|
18
|
+
* @property {object.<string, boolean>} flags
|
19
|
+
* FT.com feature flags
|
20
|
+
* @property {string} variant
|
21
|
+
* color variant of the follow button
|
22
|
+
*/
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Create a follow plus instant alerts button component
|
26
|
+
* @public
|
27
|
+
* @param {FollowProperties}
|
28
|
+
* @returns {React.ReactElement}
|
29
|
+
*/
|
30
|
+
|
31
|
+
export default function FollowPlusInstantAlerts({ conceptId, name, csrfToken, setFollowButtonStateToSelected, cacheablePersonalisedUrl, setInstantAlertsOn, flags, variant }) {
|
32
|
+
if (!flags.myFtApiWrite) {
|
33
|
+
return null;
|
34
|
+
}
|
35
|
+
|
36
|
+
const dynamicFormAttributes = setFollowButtonStateToSelected && cacheablePersonalisedUrl ? {
|
37
|
+
'action': `/myft/remove/${conceptId}`,
|
38
|
+
'data-js-action': `/__myft/api/core/followed/concept/${conceptId}?method=delete`
|
39
|
+
} : {
|
40
|
+
'action': `/myft/add/${conceptId}`,
|
41
|
+
'data-js-action': `/__myft/api/core/followed/concept/${conceptId}?method=put`
|
42
|
+
};
|
43
|
+
|
44
|
+
const dynamicButtonAttributes = setFollowButtonStateToSelected && cacheablePersonalisedUrl ? {
|
45
|
+
'aria-label': `Added ${name} to myFT: click to manage alert preferences or remove from myFT`,
|
46
|
+
'title': `Manage ${name} alert preferences or remove from myFT`,
|
47
|
+
'data-alternate-label': `Add to myFT: ${name}`,
|
48
|
+
'aria-pressed': true,
|
49
|
+
'data-alternate-text': 'Add to myFT'
|
50
|
+
} : {
|
51
|
+
'aria-label': `Add ${name} to myFT`,
|
52
|
+
'title': `Add ${name} to myFT`,
|
53
|
+
'data-alternate-label': `Added ${name} to myFT: click to manage alert preferences or remove from myFT`,
|
54
|
+
'aria-pressed': false,
|
55
|
+
'data-alternate-text': 'Added'
|
56
|
+
};
|
57
|
+
|
58
|
+
const buttonText = setFollowButtonStateToSelected && cacheablePersonalisedUrl ?
|
59
|
+
'Added' :
|
60
|
+
'Add to myFT';
|
61
|
+
|
62
|
+
return (
|
63
|
+
<form
|
64
|
+
{...dynamicFormAttributes}
|
65
|
+
className="n-myft-ui n-myft-ui--follow"
|
66
|
+
method="GET"
|
67
|
+
data-myft-ui="follow"
|
68
|
+
data-concept-id={conceptId}
|
69
|
+
data-myft-ui-variant="followPlusInstantAlerts">
|
70
|
+
<div
|
71
|
+
className="n-myft-ui__announcement o-normalise-visually-hidden"
|
72
|
+
aria-live="assertive"
|
73
|
+
data-pressed-text={`Now following ${name}.`}
|
74
|
+
data-unpressed-text={`No longer following ${name}.`}
|
75
|
+
></div>
|
76
|
+
<CsrfToken
|
77
|
+
cacheablePersonalisedUrl={cacheablePersonalisedUrl}
|
78
|
+
csrfToken={csrfToken}
|
79
|
+
/>
|
80
|
+
<button
|
81
|
+
{...dynamicButtonAttributes}
|
82
|
+
className={
|
83
|
+
`n-myft-follow-button n-myft-follow-button--instant-alerts
|
84
|
+
${setInstantAlertsOn ? 'n-myft-follow-button--instant-alerts--on' : ''}
|
85
|
+
${variant ? `n-myft-follow-button--${variant}` : ''}`
|
86
|
+
}
|
87
|
+
data-concept-id={conceptId}
|
88
|
+
data-trackable="follow"
|
89
|
+
type="submit"
|
90
|
+
data-component-id="myft-follow-plus-instant-alerts">
|
91
|
+
{buttonText}
|
92
|
+
</button>
|
93
|
+
</form>
|
94
|
+
);
|
95
|
+
};
|
@@ -0,0 +1,76 @@
|
|
1
|
+
const toggleInstantAlertsClass = ({ instantAlertsOn,followPlusInstantAlerts }) => {
|
2
|
+
// Update the button icon to reflect the instant alert preference
|
3
|
+
if (instantAlertsOn) {
|
4
|
+
followPlusInstantAlerts.classList.add('n-myft-follow-button--instant-alerts--on');
|
5
|
+
} else {
|
6
|
+
followPlusInstantAlerts.classList.remove('n-myft-follow-button--instant-alerts--on');
|
7
|
+
}
|
8
|
+
};
|
9
|
+
|
10
|
+
const instantAlertsIconLoad = ({ event, followPlusInstantAlerts }) => {
|
11
|
+
const modalConceptId = followPlusInstantAlerts.dataset.conceptId;
|
12
|
+
|
13
|
+
if (!event || !modalConceptId) {
|
14
|
+
return;
|
15
|
+
}
|
16
|
+
|
17
|
+
const currentConcept = event.detail.items
|
18
|
+
.find(item => item && item.uuid === modalConceptId);
|
19
|
+
|
20
|
+
if (!currentConcept) {
|
21
|
+
return;
|
22
|
+
}
|
23
|
+
|
24
|
+
|
25
|
+
const instantAlertsOn = Boolean(currentConcept && currentConcept._rel && currentConcept._rel.instant);
|
26
|
+
toggleInstantAlertsClass({instantAlertsOn, followPlusInstantAlerts });
|
27
|
+
};
|
28
|
+
|
29
|
+
const instantAlertsIconUpdate = ({ event, followPlusInstantAlerts }) => {
|
30
|
+
const modalConceptId = followPlusInstantAlerts.dataset.conceptId;
|
31
|
+
|
32
|
+
if (!event || !modalConceptId) {
|
33
|
+
return;
|
34
|
+
}
|
35
|
+
|
36
|
+
const currentConcept = event.detail.results
|
37
|
+
.find(item => item && item.subject && item.subject.properties && item.subject.properties.uuid === modalConceptId);
|
38
|
+
|
39
|
+
if (!currentConcept) {
|
40
|
+
return;
|
41
|
+
}
|
42
|
+
|
43
|
+
|
44
|
+
const instantAlertsOn = Boolean(currentConcept && currentConcept.rel && currentConcept.rel.properties && currentConcept.rel.properties.instant);
|
45
|
+
toggleInstantAlertsClass({instantAlertsOn, followPlusInstantAlerts });
|
46
|
+
};
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
const sendModalToggleEvent = ({ followPlusInstantAlerts }) => {
|
52
|
+
const preferenceModalToggleEvent = new CustomEvent('myft.preference-modal.show-hide.toggle', { bubbles: true });
|
53
|
+
followPlusInstantAlerts.dispatchEvent(preferenceModalToggleEvent);
|
54
|
+
followPlusInstantAlerts.classList.toggle('n-myft-follow-button--instant-alerts--open');
|
55
|
+
|
56
|
+
};
|
57
|
+
|
58
|
+
|
59
|
+
export default () => {
|
60
|
+
/**
|
61
|
+
* This feature is part of a test
|
62
|
+
* Therefore we have built it to work within the known parameters of that test
|
63
|
+
* For example we know it will only once appear in the page next to the primary add button on articles
|
64
|
+
* If this was to be used in other locations it would need some additional work to avoid being singleton
|
65
|
+
*/
|
66
|
+
const followPlusInstantAlerts = document.querySelector('[data-component-id="myft-follow-plus-instant-alerts"]');
|
67
|
+
|
68
|
+
if (!followPlusInstantAlerts) {
|
69
|
+
return;
|
70
|
+
}
|
71
|
+
|
72
|
+
followPlusInstantAlerts.addEventListener('click', () => sendModalToggleEvent({followPlusInstantAlerts}));
|
73
|
+
|
74
|
+
document.body.addEventListener('myft.user.followed.concept.load', (event) => instantAlertsIconLoad({event, followPlusInstantAlerts}));
|
75
|
+
document.body.addEventListener('myft.user.followed.concept.update', (event) => instantAlertsIconUpdate({event, followPlusInstantAlerts}));
|
76
|
+
};
|
@@ -0,0 +1,64 @@
|
|
1
|
+
@import '../../../mixins/lozenge/main.scss';
|
2
|
+
|
3
|
+
@import "@financial-times/o-icons/main";
|
4
|
+
@import "@financial-times/o-colors/main";
|
5
|
+
|
6
|
+
@mixin bellOffIcon($color, $background) {
|
7
|
+
@include oIconsContent(
|
8
|
+
$icon-name: 'mute-notifications',
|
9
|
+
$color: oColorsMix(
|
10
|
+
$color: $color,
|
11
|
+
$background: $background,
|
12
|
+
$percentage: 60,
|
13
|
+
),
|
14
|
+
$size: 10
|
15
|
+
);
|
16
|
+
background-size: 21px;
|
17
|
+
}
|
18
|
+
|
19
|
+
@mixin bellOnIcon($color) {
|
20
|
+
@include oIconsContent(
|
21
|
+
$icon-name: 'notifications',
|
22
|
+
$color: $color,
|
23
|
+
$size: 10
|
24
|
+
);
|
25
|
+
background-size: 21px;
|
26
|
+
}
|
27
|
+
|
28
|
+
@mixin arrowIcon($icon-name, $color) {
|
29
|
+
content: "";
|
30
|
+
@include oIconsContent(
|
31
|
+
$icon-name: $icon-name,
|
32
|
+
$color: $color,
|
33
|
+
$size: 10
|
34
|
+
);
|
35
|
+
background-size: 21px;
|
36
|
+
background-position: 50% 41%;
|
37
|
+
margin-left: 12px;
|
38
|
+
}
|
39
|
+
|
40
|
+
@mixin myftFollowButtonPlusToggleIcons($theme: standard) {
|
41
|
+
@include withTheme($theme) {
|
42
|
+
&.n-myft-follow-button--instant-alerts[aria-pressed=true]::before {
|
43
|
+
@include bellOffIcon(getThemeColor(text), getThemeColor(background));
|
44
|
+
}
|
45
|
+
|
46
|
+
&.n-myft-follow-button--instant-alerts--on[aria-pressed=true]::before {
|
47
|
+
@include bellOnIcon(getThemeColor(text));
|
48
|
+
}
|
49
|
+
|
50
|
+
&.n-myft-follow-button--instant-alerts[aria-pressed=true]::after {
|
51
|
+
@include arrowIcon('arrow-down', getThemeColor(text));
|
52
|
+
}
|
53
|
+
|
54
|
+
&.n-myft-follow-button--instant-alerts--open[aria-pressed=true]::after {
|
55
|
+
@include arrowIcon('arrow-up', getThemeColor(text));
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
@each $theme in map-keys($myft-lozenge-themes) {
|
61
|
+
.n-myft-follow-button#{getThemeModifier($theme)} {
|
62
|
+
@include myftFollowButtonPlusToggleIcons($theme);
|
63
|
+
}
|
64
|
+
}
|
@@ -0,0 +1,182 @@
|
|
1
|
+
import myFtClient from 'next-myft-client';
|
2
|
+
import getToken from '../../../myft/ui/lib/get-csrf-token';
|
3
|
+
|
4
|
+
const csrfToken = getToken();
|
5
|
+
|
6
|
+
const renderError = ({ message, preferencesModal }) => {
|
7
|
+
const errorElement = preferencesModal.querySelector('[data-component-id="myft-preference-modal-error"]');
|
8
|
+
|
9
|
+
errorElement.innerHTML = message;
|
10
|
+
};
|
11
|
+
|
12
|
+
/**
|
13
|
+
* This preference modal is part of a test
|
14
|
+
* Therefore we have built the positioning function to work within the known parameters of that test
|
15
|
+
* For example we know it will only appear next to the primary add button on articles
|
16
|
+
* If this was to be used in other locations this function would need further improvements
|
17
|
+
*/
|
18
|
+
const positionModal = ({ event, preferencesModal } = {}) => {
|
19
|
+
const eventTrigger = event.target;
|
20
|
+
|
21
|
+
if (!eventTrigger) {
|
22
|
+
return;
|
23
|
+
}
|
24
|
+
|
25
|
+
const modalHorizontalCentering = (eventTrigger.offsetLeft + (eventTrigger.offsetWidth / 2)) - (preferencesModal.offsetWidth / 2);
|
26
|
+
const verticalPadding = 15;
|
27
|
+
const leftPadding = '5px';
|
28
|
+
|
29
|
+
preferencesModal.style.top = `${eventTrigger.offsetTop + eventTrigger.offsetHeight + verticalPadding}px`;
|
30
|
+
preferencesModal.style.left = `${modalHorizontalCentering}px`;
|
31
|
+
|
32
|
+
const modalPositionRelativeToScreen = preferencesModal.getBoundingClientRect();
|
33
|
+
|
34
|
+
if (modalPositionRelativeToScreen.left < 0) {
|
35
|
+
preferencesModal.style.left = leftPadding;
|
36
|
+
}
|
37
|
+
|
38
|
+
if (modalPositionRelativeToScreen.right > window.screen.width) {
|
39
|
+
const triggerRightPosition = eventTrigger.offsetLeft + eventTrigger.offsetWidth;
|
40
|
+
const modalShiftLeftPosition = triggerRightPosition - preferencesModal.offsetWidth;
|
41
|
+
|
42
|
+
preferencesModal.style.left = modalShiftLeftPosition > 0
|
43
|
+
? `${modalShiftLeftPosition}px`
|
44
|
+
: leftPadding;
|
45
|
+
}
|
46
|
+
};
|
47
|
+
|
48
|
+
const preferenceModalShowAndHide = ({ event, preferencesModal }) => {
|
49
|
+
preferencesModal.classList.toggle('n-myft-ui__preferences-modal--show');
|
50
|
+
|
51
|
+
if (preferencesModal.classList.contains('n-myft-ui__preferences-modal--show')) {
|
52
|
+
positionModal({ event, preferencesModal });
|
53
|
+
} else {
|
54
|
+
// Remove existing errors when hiding the modal
|
55
|
+
renderError({
|
56
|
+
message: '',
|
57
|
+
preferencesModal,
|
58
|
+
});
|
59
|
+
}
|
60
|
+
};
|
61
|
+
|
62
|
+
const removeTopic = async ({ event, conceptId, preferencesModal }) => {
|
63
|
+
event.target.setAttribute('disabled', true);
|
64
|
+
|
65
|
+
try {
|
66
|
+
await myFtClient.remove('user', null, 'followed', 'concept', conceptId, { token: csrfToken });
|
67
|
+
|
68
|
+
preferenceModalShowAndHide({ preferencesModal });
|
69
|
+
|
70
|
+
} catch (error) {
|
71
|
+
renderError({ message: 'Sorry, we are unable to remove this topic. Please try again later or try from <a href="/myft">myFT</a>', preferencesModal });
|
72
|
+
}
|
73
|
+
|
74
|
+
event.target.removeAttribute('disabled');
|
75
|
+
};
|
76
|
+
|
77
|
+
const getAlertsPreferences = async ({ event, preferencesModal }) => {
|
78
|
+
const preferencesList = preferencesModal.querySelector('[data-component-id="myft-preferences-modal-list"]');
|
79
|
+
|
80
|
+
if (!preferencesList) {
|
81
|
+
return;
|
82
|
+
}
|
83
|
+
const addedTextBuffer = [];
|
84
|
+
|
85
|
+
event.detail.items.forEach(item => {
|
86
|
+
if (item.uuid === 'email-instant') {
|
87
|
+
addedTextBuffer.push(' email');
|
88
|
+
}
|
89
|
+
else if (item.uuid === 'app-instant') {
|
90
|
+
addedTextBuffer.push(' app');
|
91
|
+
}
|
92
|
+
});
|
93
|
+
|
94
|
+
try {
|
95
|
+
// We need the service worker registration to check for a subscription
|
96
|
+
const serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
97
|
+
const subscription = await serviceWorkerRegistration.pushManager.getSubscription();
|
98
|
+
if (subscription) {
|
99
|
+
addedTextBuffer.push('browser');
|
100
|
+
}
|
101
|
+
|
102
|
+
} catch (error) {
|
103
|
+
// eslint-disable-next-line no-console
|
104
|
+
console.warn('There was an error fetching the browser notification preferences', error);
|
105
|
+
}
|
106
|
+
const alertsEnabledText = `Your delivery channels: ${addedTextBuffer.join(', ')}.`;
|
107
|
+
const alertsDisabledText = 'You have previously disabled all delivery channels';
|
108
|
+
preferencesList.innerHTML = addedTextBuffer.length > 0 ? alertsEnabledText : alertsDisabledText;
|
109
|
+
};
|
110
|
+
|
111
|
+
const setCheckboxForAlertConcept = ({ event, preferencesModal }) => {
|
112
|
+
const conceptId = preferencesModal.dataset.conceptId;
|
113
|
+
const instantAlertsCheckbox = preferencesModal.querySelector('[data-component-id="myft-preferences-modal-checkbox"]');
|
114
|
+
// search through all the concepts that the user has followed and check whether
|
115
|
+
// 1. the concept which this instant alert modal controls is within them, AND;
|
116
|
+
// 2. the said concept has instant alert enabled
|
117
|
+
// if so, check the checkbox within the modal
|
118
|
+
const currentConcept = event.detail.items.find(item => item && item.uuid === conceptId);
|
119
|
+
if (currentConcept && currentConcept._rel && currentConcept._rel.instant) {
|
120
|
+
instantAlertsCheckbox.checked = true;
|
121
|
+
} else {
|
122
|
+
instantAlertsCheckbox.checked = false;
|
123
|
+
}
|
124
|
+
};
|
125
|
+
|
126
|
+
const toggleInstantAlertsPreference = async ({ event, conceptId, preferencesModal }) => {
|
127
|
+
const instantAlertsToggle = event.target;
|
128
|
+
|
129
|
+
instantAlertsToggle.setAttribute('disabled', true);
|
130
|
+
|
131
|
+
const data = {
|
132
|
+
token: csrfToken
|
133
|
+
};
|
134
|
+
|
135
|
+
if (instantAlertsToggle.checked) {
|
136
|
+
data._rel = {instant: 'true'};
|
137
|
+
} else {
|
138
|
+
data._rel = {instant: 'false'};
|
139
|
+
}
|
140
|
+
|
141
|
+
try {
|
142
|
+
await myFtClient.updateRelationship('user', null, 'followed', 'concept', conceptId, data);
|
143
|
+
} catch (error) {
|
144
|
+
renderError({
|
145
|
+
message: 'Sorry, we are unable to change your instant alert preference. Please try again later or try from <a href="/myft">myFT</a>',
|
146
|
+
preferencesModal
|
147
|
+
});
|
148
|
+
|
149
|
+
instantAlertsToggle.checked = instantAlertsToggle.checked
|
150
|
+
? false
|
151
|
+
: true;
|
152
|
+
}
|
153
|
+
|
154
|
+
instantAlertsToggle.removeAttribute('disabled');
|
155
|
+
};
|
156
|
+
|
157
|
+
export default () => {
|
158
|
+
/**
|
159
|
+
* This feature is part of a test
|
160
|
+
* Therefore we have built it to work within the known parameters of that test
|
161
|
+
* For example we know it will only once appear in the page next to the primary add button on articles
|
162
|
+
* If this was to be used in other locations it would need some additional work to avoid being singleton
|
163
|
+
*/
|
164
|
+
const preferencesModal = document.querySelector('[data-component-id="myft-preferences-modal"]');
|
165
|
+
const conceptId = preferencesModal.dataset.conceptId;
|
166
|
+
|
167
|
+
if (!preferencesModal || !conceptId) {
|
168
|
+
return;
|
169
|
+
}
|
170
|
+
|
171
|
+
const removeTopicButton = preferencesModal.querySelector('[data-component-id="myft-preference-modal-remove"]');
|
172
|
+
const instantAlertsCheckbox = preferencesModal.querySelector('[data-component-id="myft-preferences-modal-checkbox"]');
|
173
|
+
|
174
|
+
removeTopicButton.addEventListener('click', event => removeTopic({ event, conceptId, preferencesModal }));
|
175
|
+
instantAlertsCheckbox.addEventListener('change', event => toggleInstantAlertsPreference({ event, conceptId, preferencesModal }));
|
176
|
+
|
177
|
+
document.addEventListener('myft.preference-modal.show-hide.toggle', event => preferenceModalShowAndHide({ event, preferencesModal }));
|
178
|
+
|
179
|
+
document.addEventListener('myft.user.preferred.preference.load', (event) => getAlertsPreferences({ event, preferencesModal }));
|
180
|
+
|
181
|
+
document.body.addEventListener('myft.user.followed.concept.load', (event) => setCheckboxForAlertConcept({ event, preferencesModal }));
|
182
|
+
};
|
@@ -0,0 +1,59 @@
|
|
1
|
+
@import '@financial-times/o-colors/main';
|
2
|
+
@import '@financial-times/o-forms/main';
|
3
|
+
@include oForms();
|
4
|
+
|
5
|
+
.n-myft-ui__preferences-modal {
|
6
|
+
display: none;
|
7
|
+
visibility: hidden;
|
8
|
+
position: absolute;
|
9
|
+
color: oColorsByName('black');
|
10
|
+
background-color: oColorsByName('white-80');
|
11
|
+
border-radius: 10px;
|
12
|
+
border: 2px solid oColorsByName('black-5');
|
13
|
+
width: 275px;
|
14
|
+
z-index: 9999999;
|
15
|
+
}
|
16
|
+
|
17
|
+
.n-myft-ui__preferences-modal__content {
|
18
|
+
position: relative;
|
19
|
+
margin: 20px 20px 24px;
|
20
|
+
}
|
21
|
+
|
22
|
+
.n-myft-ui__preferences-modal__checkbox__message {
|
23
|
+
@include oTypographySans($scale: 0, $weight: 'semibold', $style: 'normal');
|
24
|
+
}
|
25
|
+
|
26
|
+
.n-myft-ui__preferences-modal__text {
|
27
|
+
@include oTypographySans($scale: -1, $weight: 'regular', $style: 'normal');
|
28
|
+
margin-bottom: 0;
|
29
|
+
}
|
30
|
+
|
31
|
+
.n-myft-ui__preferences-modal__remove-button {
|
32
|
+
margin-top: 20px;
|
33
|
+
height: 32px;
|
34
|
+
width: 100%;
|
35
|
+
border-radius: 20px;
|
36
|
+
border: 2px solid oColorsByName('black-20');
|
37
|
+
background-color: oColorsByName('white-80');
|
38
|
+
@include oTypographySans($scale: -1, $weight: 'regular', $style: 'normal');
|
39
|
+
color: oColorsByName('black-80');
|
40
|
+
}
|
41
|
+
|
42
|
+
.n-myft-ui__preferences-modal__remove-button:not([disabled]):hover,
|
43
|
+
.n-myft-ui__preferences-modal__remove-button:not([disabled]):focus {
|
44
|
+
border: 2px solid oColorsByName('claret-60');
|
45
|
+
background-color: oColorsMix($color: 'claret-60', $percentage: 20);
|
46
|
+
color: oColorsByName('claret-60');
|
47
|
+
}
|
48
|
+
|
49
|
+
.n-myft-ui__preferences-modal--show {
|
50
|
+
display: block;
|
51
|
+
visibility: visible;
|
52
|
+
}
|
53
|
+
|
54
|
+
.n-myft-ui__preferences-modal-error {
|
55
|
+
display: block;
|
56
|
+
@include oTypographySans($scale: -1, $weight: 'regular', $style: 'normal');
|
57
|
+
color: oColorsByName('claret');
|
58
|
+
text-align: center;
|
59
|
+
}
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* @typedef {Object} PreferencesProperties
|
5
|
+
* @property {string} conceptId
|
6
|
+
* Concept id of the concept which the modal controls
|
7
|
+
* @property {Record<string, boolean>} flags
|
8
|
+
* FT.com feature flags
|
9
|
+
* @property {boolean} visible
|
10
|
+
* Controls the visibility of the modal
|
11
|
+
*/
|
12
|
+
|
13
|
+
/**
|
14
|
+
* Create a popup modal to manage myFT alert preferences
|
15
|
+
* @public
|
16
|
+
* @param {PreferencesProperties}
|
17
|
+
* @returns {React.ReactElement}
|
18
|
+
*/
|
19
|
+
export default function InstantAlertsPreferencesModal({ flags, conceptId, visible }) {
|
20
|
+
if (!flags.myFtApiWrite) {
|
21
|
+
return null;
|
22
|
+
}
|
23
|
+
|
24
|
+
return (
|
25
|
+
<div
|
26
|
+
className={`n-myft-ui__preferences-modal ${visible ? 'n-myft-ui__preferences-modal--show' : ''}`}
|
27
|
+
data-component-id="myft-preferences-modal"
|
28
|
+
data-concept-id={conceptId}
|
29
|
+
>
|
30
|
+
<div className="n-myft-ui__preferences-modal__content">
|
31
|
+
<span className="o-forms-input o-forms-input--checkbox">
|
32
|
+
<label htmlFor="receive-instant-alerts">
|
33
|
+
<input
|
34
|
+
id="receive-instant-alerts"
|
35
|
+
type="checkbox"
|
36
|
+
name="receive-instant-alerts"
|
37
|
+
value="receive-instant-alerts"
|
38
|
+
data-component-id="myft-preferences-modal-checkbox"
|
39
|
+
/>
|
40
|
+
<span className="o-forms-input__label n-myft-ui__preferences-modal__checkbox__message">
|
41
|
+
Get instant alerts for this topic
|
42
|
+
</span>
|
43
|
+
</label>
|
44
|
+
</span>
|
45
|
+
|
46
|
+
<p data-component-id="myft-preferences-modal-list" className="n-myft-ui__preferences-modal__text"></p>
|
47
|
+
<a className="n-myft-ui__preferences-modal__text" href="/myft/alerts">Manage your preferences here</a>
|
48
|
+
<span className="n-myft-ui__preferences-modal-error" data-component-id="myft-preference-modal-error"></span>
|
49
|
+
<button className="n-myft-ui__preferences-modal__remove-button" data-component-id="myft-preference-modal-remove">Remove from myFT</button>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
);
|
53
|
+
};
|
package/demos/app.js
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require('sucrase/register');
|
2
|
+
|
1
3
|
const nExpress = require('@financial-times/n-express');
|
2
4
|
const chalk = require('chalk');
|
3
5
|
const errorHighlight = chalk.bold.red;
|
@@ -12,7 +14,9 @@ const fixtures = {
|
|
12
14
|
collections: require('./fixtures/collections'),
|
13
15
|
conceptList: require('./fixtures/concept-list'),
|
14
16
|
pinButton: require('./fixtures/pin-button'),
|
15
|
-
instantAlert: require('./fixtures/instant-alert')
|
17
|
+
instantAlert: require('./fixtures/instant-alert'),
|
18
|
+
followPlusInstantAlerts: require('./fixtures/jsx/follow-plus-instant-alerts'),
|
19
|
+
instantAlertsPreferencesModal: require('./fixtures/jsx/preferences-modal'),
|
16
20
|
};
|
17
21
|
|
18
22
|
const app = module.exports = nExpress({
|
package/demos/src/demo.scss
CHANGED
@@ -7,7 +7,9 @@ $system-code: "n-myft-ui-demo";
|
|
7
7
|
@import '../../components/concept-pill/main';
|
8
8
|
@import '../../components/button/main';
|
9
9
|
@import '../../components/follow-button/main';
|
10
|
+
@import '../../components/jsx/follow-plus-instant-alerts/main';
|
10
11
|
@import '../../components/onboarding-cta/main';
|
12
|
+
@import '../../components/jsx/preferences-modal/main';
|
11
13
|
|
12
14
|
@import '../../components/digest-promo/main';
|
13
15
|
.n-myft-digest-promo {
|