@financial-times/n-myft-ui 30.3.0 → 30.4.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.
- package/CODEOWNERS +1 -0
- package/build-state/npm-shrinkwrap.json +610 -24
- 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 +5 -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 {
|