@financial-times/n-myft-ui 27.1.0 → 28.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,8 @@
3
3
  data-content-id="{{contentId}}"
4
4
  data-myft-ui="saved"
5
5
  action="/myft/save/{{contentId}}"
6
- data-js-action="/__myft/api/core/saved/content/{{contentId}}?method=put">
6
+ data-js-action="/__myft/api/core/saved/content/{{contentId}}?method=put"
7
+ {{#ifEquals @root.flags.professorLists 'variant'}}data-myft-ui-variant="createListAndSaveArticleVariant"{{/ifEquals}}>
7
8
  {{> n-myft-ui/components/csrf-token/input}}
8
9
  <div
9
10
  class="n-myft-ui__announcement o-normalise-visually-hidden"
@@ -2,12 +2,12 @@
2
2
  // the detail => https://github.com/date-fns/date-fns/blob/HEAD/CHANGELOG.md#200---2019-08-20
3
3
  // By adding validation for dates before their functions allows us to know it when unexpected value passed.
4
4
 
5
- import isTodayOriginal from "date-fns/src/isToday";
6
- import isAfterOriginal from "date-fns/src/isAfter";
7
- import addMinutesOriginal from "date-fns/src/addMinutes";
8
- import startOfDayOriginal from "date-fns/src/startOfDay";
9
- import isValidOriginal from "date-fns/src/isValid";
10
- import parseISO from "date-fns/src/parseISO";
5
+ import isTodayOriginal from 'date-fns/src/isToday';
6
+ import isAfterOriginal from 'date-fns/src/isAfter';
7
+ import addMinutesOriginal from 'date-fns/src/addMinutes';
8
+ import startOfDayOriginal from 'date-fns/src/startOfDay';
9
+ import isValidOriginal from 'date-fns/src/isValid';
10
+ import parseISO from 'date-fns/src/parseISO';
11
11
 
12
12
  const isValid = (date) => {
13
13
  if (!isValidOriginal(date)) {
package/karma.conf.js CHANGED
@@ -153,13 +153,6 @@ module.exports = function (karma) {
153
153
  os: 'Windows',
154
154
  os_version: '10'
155
155
  },
156
- ie11: {
157
- base: 'BrowserStack',
158
- browser: 'IE',
159
- browser_version: '11',
160
- os: 'Windows',
161
- os_version: '7'
162
- },
163
156
  safari: {
164
157
  base: 'BrowserStack',
165
158
  os: 'OS X',
package/myft/main.scss CHANGED
@@ -173,3 +173,148 @@ $spacing-unit: 20px;
173
173
  }
174
174
 
175
175
  }
176
+
177
+ .share-nav {
178
+ &.data-overlap-initialised {
179
+ .o-overlay {
180
+ transition: opacity 0.15s ease-in;
181
+ opacity: 0;
182
+ z-index: -1;
183
+ }
184
+ }
185
+
186
+ .myft-ui-create-list-variant {
187
+ border-radius: 10px;
188
+ border: 1px solid oColorsByName('black-5');
189
+ background: oColorsByName('white-80');
190
+
191
+ .o-overlay__heading {
192
+ border-radius: 10px 10px 0 0;
193
+ background: oColorsByName('white-60');
194
+ @include oTypographySans($scale: 2);
195
+ color: oColorsByName('black-80');
196
+ }
197
+
198
+ .o-overlay__content {
199
+ @include oTypographySans($scale: 0);
200
+ color: oColorsByName('black-80');
201
+ padding: 0;
202
+ }
203
+
204
+ .o-overlay__title {
205
+ margin: 8px 14px 0 8px;
206
+ }
207
+
208
+ &-container {
209
+ display: block;
210
+ width: 340px;
211
+ top: 115.5px;
212
+ left: 50px;
213
+ }
214
+
215
+ &-add {
216
+ border: 0;
217
+ background: none;
218
+ @include oTypographySans($scale: 1, $weight: 'semibold');
219
+ color: oColorsByName('black-80');
220
+
221
+ padding-left: 0;
222
+ margin-left: -8px;
223
+
224
+ &:hover {
225
+ text-decoration: underline;
226
+ }
227
+
228
+ &::before {
229
+ content: '';
230
+ @include oIconsContent(
231
+ 'plus',
232
+ oColorsByName('black-80'),
233
+ 28,
234
+ $iconset-version: 1
235
+ );
236
+ vertical-align: middle;
237
+ margin-top: -2px;
238
+ }
239
+ }
240
+
241
+ &-add-description {
242
+ margin: 4px 0;
243
+ }
244
+
245
+ &-heading {
246
+ &::before {
247
+ content: '';
248
+ @include oIconsContent(
249
+ 'tick',
250
+ oColorsByName('teal'),
251
+ 32,
252
+ $iconset-version: 1
253
+ );
254
+ vertical-align: middle;
255
+ margin-top: -2px;
256
+ }
257
+ }
258
+
259
+ &-footer {
260
+ border-top: 1px solid oColorsByName('black-5');
261
+ padding: 16px;
262
+ }
263
+
264
+ &-icon {
265
+ &::before {
266
+ content: "";
267
+ display: inline-block;
268
+ background-repeat: no-repeat;
269
+ background-size: contain;
270
+ background-position: 50%;
271
+ background-color: transparent;
272
+ background-image: url(https://www.ft.com/__origami/service/image/v2/images/raw/ftlogo-v1:brand-myft?source=next-article);
273
+ width: 42px;
274
+ height: 42px;
275
+ vertical-align: middle;
276
+ margin-top: -2px;
277
+ }
278
+
279
+ &-visually-hidden {
280
+ clip: rect(0 0 0 0);
281
+ clip-path: inset(50%);
282
+ height: 1px;
283
+ overflow: hidden;
284
+ position: absolute;
285
+ white-space: nowrap;
286
+ width: 1px;
287
+ }
288
+ }
289
+
290
+ &-form {
291
+ display: flex;
292
+ width: calc(100% - 32px);
293
+ justify-content: space-between;
294
+ height: 40px;
295
+ gap: 8px;
296
+ padding: 0 16px 16px;
297
+
298
+ & > * {
299
+ flex: 1 1 auto;
300
+ }
301
+
302
+ .o-forms-input {
303
+ margin-top: 0;
304
+ }
305
+ }
306
+
307
+ &-lists {
308
+ padding: 16px 16px 0;
309
+ @include oTypographySans($scale: 1);
310
+ &-text {
311
+ @include oTypographySans($weight: 'semibold');
312
+ color: oColorsByName('black-80');
313
+ margin-bottom: 16px;
314
+ }
315
+ &-container {
316
+ margin-top: 0;
317
+ }
318
+ }
319
+ }
320
+ }
package/myft/ui/lists.js CHANGED
@@ -6,6 +6,7 @@ import nNotification from 'n-notification';
6
6
  import { uuid } from 'n-ui-foundations';
7
7
  import getToken from './lib/get-csrf-token';
8
8
  import oForms from 'o-forms';
9
+ import openSaveArticleToListVariant from './save-article-to-list-variant';
9
10
 
10
11
  const delegate = new Delegate(document.body);
11
12
  const csrfToken = getToken();
@@ -161,6 +162,10 @@ function showArticleSavedOverlay (contentId) {
161
162
  showListsOverlay('Article saved', `/myft/list?fragment=true&fromArticleSaved=true&contentId=${contentId}`, contentId);
162
163
  }
163
164
 
165
+ function showCreateListAndAddArticleOverlay (contentId, name = 'myft-ui-create-list-variant') {
166
+ return openSaveArticleToListVariant(name, contentId);
167
+ }
168
+
164
169
  function handleArticleSaved (contentId) {
165
170
  return myFtClient.getAll('created', 'list')
166
171
  .then(createdLists => createdLists.filter(list => !list.isRedirect))
@@ -171,10 +176,27 @@ function handleArticleSaved (contentId) {
171
176
  });
172
177
  }
173
178
 
179
+ function openCreateListAndAddArticleOverlay (contentId) {
180
+ return myFtClient.getAll('created', 'list')
181
+ .then(createdLists => createdLists.filter(list => !list.isRedirect))
182
+ .then(createdLists => {
183
+ return !createdLists.length ? showCreateListAndAddArticleOverlay(contentId) : showArticleSavedOverlay(contentId);
184
+ });
185
+ }
186
+
174
187
  function initialEventListeners () {
175
188
 
176
189
  document.body.addEventListener('myft.user.saved.content.add', event => {
177
190
  const contentId = event.detail.subject;
191
+
192
+ // Checks if the createListAndSaveArticle variant is active
193
+ // and will show the variant overlay if the user has no lists,
194
+ // otherwise it will show the classic overlay
195
+ const createListVariant = event.currentTarget.querySelector('[data-myft-ui-variant="createListAndSaveArticleVariant"]');
196
+ if (createListVariant) {
197
+ return openCreateListAndAddArticleOverlay(contentId);
198
+ }
199
+
178
200
  handleArticleSaved(contentId);
179
201
  });
180
202
 
@@ -0,0 +1,369 @@
1
+ import Overlay from 'o-overlay';
2
+ import myFtClient from 'next-myft-client';
3
+ import { uuid } from 'n-ui-foundations';
4
+ import getToken from './lib/get-csrf-token';
5
+
6
+ const csrfToken = getToken();
7
+
8
+ let lists;
9
+
10
+ export default async function showSaveArticleToListVariant (name, contentId) {
11
+ try {
12
+ await openSaveArticleToListVariant (name, contentId);
13
+ } catch(error) {
14
+ handleError(error);
15
+ }
16
+ }
17
+
18
+ async function openSaveArticleToListVariant (name, contentId) {
19
+ function createList (list) {
20
+ if(!list) {
21
+ return;
22
+ }
23
+
24
+ myFtClient.add('user', null, 'created', 'list', uuid(), { name: list, token: csrfToken })
25
+ .then(detail => {
26
+ myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((createdList) => {
27
+ lists.push({ name: list, uuid: createdList.actorId, checked: true });
28
+ const listElement = ListsElement(lists, addToList, removeFromList);
29
+ const overlayContent = document.querySelector('.o-overlay__content');
30
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
31
+ const announceListContainer = document.querySelector('.myft-ui-create-list-variant-announcement');
32
+ announceListContainer.textContent = `${list} created`;
33
+ triggerCreateListEvent(contentId);
34
+ contentElement.addEventListener('click', openFormHandler, { once: true });
35
+ });
36
+ });
37
+ }
38
+
39
+ function addToList (list) {
40
+ if(!list) {
41
+ return;
42
+ }
43
+
44
+ myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then(() => {
45
+ const indexToUpdate = lists.indexOf(list);
46
+ lists[indexToUpdate] = { ...lists[indexToUpdate], checked: true };
47
+ const listElement = ListsElement(lists, addToList, removeFromList);
48
+ const overlayContent = document.querySelector('.o-overlay__content');
49
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
50
+ triggerAddToListEvent(contentId);
51
+ });
52
+ }
53
+
54
+ function removeFromList (list) {
55
+ if(!list) {
56
+ return;
57
+ }
58
+
59
+ myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then(() => {
60
+ const indexToUpdate = lists.indexOf(list);
61
+ lists[indexToUpdate] = { ...lists[indexToUpdate], checked: false };
62
+ const listElement = ListsElement(lists, addToList, removeFromList);
63
+ const overlayContent = document.querySelector('.o-overlay__content');
64
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
65
+ triggerRemoveFromListEvent(contentId);
66
+ });
67
+ }
68
+
69
+ if (!lists) {
70
+ lists = await getLists(contentId);
71
+ }
72
+
73
+ const overlays = Overlay.getOverlays();
74
+ const existingOverlay = overlays[name];
75
+ if (existingOverlay) {
76
+ existingOverlay.destroy();
77
+ }
78
+
79
+ const headingElement = HeadingElement();
80
+ let [contentElement, removeDescription] = ContentElement(!lists.length);
81
+
82
+ const createListOverlay = new Overlay(name, {
83
+ html: contentElement,
84
+ heading: { title: headingElement.outerHTML },
85
+ modal: false,
86
+ parentnode: isMobile() ? '.o-share--horizontal' : '.o-share--vertical',
87
+ class: 'myft-ui-create-list-variant',
88
+ });
89
+
90
+ const realignListener = realignOverlay(window.scrollY);
91
+
92
+ function outsideClickHandler (e) {
93
+ try {
94
+ const overlayContent = document.querySelector('.o-overlay__content');
95
+ if(!overlayContent || !overlayContent.contains(e.target)) {
96
+ createListOverlay.close();
97
+ }
98
+ } catch(error) {
99
+ handleError(error);
100
+ }
101
+ }
102
+
103
+ function openFormHandler () {
104
+ try {
105
+ const formElement = FormElement(createList);
106
+ const overlayContent = document.querySelector('.o-overlay__content');
107
+ removeDescription();
108
+ overlayContent.insertAdjacentElement('beforeend', formElement);
109
+ formElement.elements[0].focus();
110
+ } catch(error) {
111
+ handleError(error);
112
+ }
113
+ }
114
+
115
+ createListOverlay.open();
116
+ createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
117
+ realignListener(data.currentTarget);
118
+
119
+ if (lists && lists.length) {
120
+ const listElement = ListsElement(lists, addToList, removeFromList);
121
+ const overlayContent = document.querySelector('.o-overlay__content');
122
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
123
+ }
124
+
125
+ contentElement.addEventListener('click', openFormHandler, { once: true });
126
+
127
+ document.querySelector('.article-content').addEventListener('click', outsideClickHandler, { once: true });
128
+ });
129
+
130
+ window.addEventListener('scroll', realignListener(createListOverlay.wrapper, window.scrollY));
131
+
132
+ window.addEventListener('oViewport.resize', () => {
133
+ realignListener(createListOverlay.wrapper);
134
+ });
135
+ }
136
+
137
+ function stringToHTMLElement (string) {
138
+ const template = document.createElement('template');
139
+ template.innerHTML = string.trim();
140
+ return template.content.firstChild;
141
+ }
142
+
143
+ function FormElement (createList) {
144
+ const formString = `
145
+ <form class="myft-ui-create-list-variant-form">
146
+ <label class="o-forms-field">
147
+ <span class="o-forms-input o-forms-input--text">
148
+ <input type="text" name="list-name" aria-label="List name">
149
+ </span>
150
+ </label>
151
+ <button class="o-buttons o-buttons--secondary" type="submit">
152
+ Save
153
+ </button>
154
+ </form>
155
+ `;
156
+
157
+ const formElement = stringToHTMLElement(formString);
158
+
159
+ function handleSubmit (event) {
160
+ try {
161
+ event.preventDefault();
162
+ event.stopPropagation();
163
+ const inputListName = formElement.querySelector('input[name="list-name"]');
164
+ createList(inputListName.value);
165
+ inputListName.value = '';
166
+ formElement.remove();
167
+ } catch(error) {
168
+ handleError(error);
169
+ }
170
+ }
171
+
172
+ formElement.querySelector('button[type="submit"]').addEventListener('click', handleSubmit);
173
+
174
+ return formElement;
175
+ }
176
+
177
+ function ContentElement (description) {
178
+ const content = `
179
+ <div class="myft-ui-create-list-variant-footer">
180
+ <button class="myft-ui-create-list-variant-add">Add to a new list</button>
181
+ ${description ? `
182
+ <p class="myft-ui-create-list-variant-add-description">Lists are a simple way to curate your content</p>
183
+ ` : ''}
184
+ </div>
185
+ `;
186
+
187
+ const contentElement = stringToHTMLElement(content);
188
+
189
+ function removeDescription () {
190
+ const descriptionElement = contentElement.querySelector('.myft-ui-create-list-variant-add-description');
191
+ if (descriptionElement) {
192
+ descriptionElement.remove();
193
+ }
194
+ }
195
+
196
+ return [contentElement, removeDescription];
197
+ }
198
+
199
+ function HeadingElement () {
200
+ const heading = `
201
+ <span class="myft-ui-create-list-variant-heading">Added to <a href="https://www.ft.com/myft/saved-articles">saved articles</a> in <span class="myft-ui-create-list-variant-icon"><span class="myft-ui-create-list-variant-icon-visually-hidden">my FT</span></span></span>
202
+ `;
203
+
204
+ return stringToHTMLElement(heading);
205
+ }
206
+
207
+ function ListsElement (lists, addToList, removeFromList) {
208
+ const currentList = document.querySelector('.myft-ui-create-list-variant-lists');
209
+ if (currentList) {
210
+ currentList.remove();
211
+ }
212
+
213
+ const listCheckboxElement = ListCheckboxElement(addToList, removeFromList);
214
+
215
+ const listsTemplate = `
216
+ <div class="myft-ui-create-list-variant-lists o-forms-field o-forms-field--optional" role="group">
217
+ <span class="myft-ui-create-list-variant-lists-text">Add to a list</span>
218
+ <span class="myft-ui-create-list-variant-lists-container o-forms-input o-forms-input--checkbox">
219
+ </span>
220
+ </div>
221
+ `;
222
+ const listsElement = stringToHTMLElement(listsTemplate);
223
+
224
+ const listsElementContainer = listsElement.querySelector('.myft-ui-create-list-variant-lists-container');
225
+
226
+ lists.map(list => listsElementContainer.insertAdjacentElement('beforeend', listCheckboxElement(list)));
227
+
228
+ return listsElement;
229
+ }
230
+
231
+ function ListCheckboxElement (addToList, removeFromList) {
232
+ return function (list) {
233
+ const listCheckbox = `<label>
234
+ <input type="checkbox" name="default" value="${list.name}" ${list.checked ? 'checked' : ''}>
235
+ <span class="o-forms-input__label">
236
+ <span class="o-normalise-visually-hidden">
237
+ ${list.checked ? 'Remove article from ' : 'Add article to ' }
238
+ </span>
239
+ ${list.name}
240
+ </span>
241
+ </label>
242
+ `;
243
+
244
+ const listCheckboxElement = stringToHTMLElement(listCheckbox);
245
+
246
+ const [ input ] = listCheckboxElement.children;
247
+
248
+ function handleCheck (event) {
249
+ event.preventDefault();
250
+ return event.target.checked ? addToList(list) : removeFromList(list);
251
+ }
252
+
253
+ input.addEventListener('click', handleCheck);
254
+
255
+ return listCheckboxElement;
256
+ };
257
+ }
258
+
259
+ function realignOverlay (originalScrollPosition) {
260
+ return function (target, currentScrollPosition) {
261
+ try {
262
+ if(currentScrollPosition && Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
263
+ return;
264
+ }
265
+
266
+ originalScrollPosition = currentScrollPosition;
267
+
268
+ target.style['min-width'] = '340px';
269
+ target.style['width'] = '100%';
270
+ target.style['margin-top'] = '-50px';
271
+ target.style['left'] = 0;
272
+
273
+ if (isMobile()) {
274
+ target.style['position'] = 'absolute';
275
+ target.style['margin-left'] = 0;
276
+ target.style['margin-top'] = 0;
277
+ target.style['top'] = calculateLargerScreenHalf(target) === 'ABOVE' ? '-120px' : '50px';
278
+ } else {
279
+ target.style['position'] = 'absolute';
280
+ target.style['margin-left'] = '45px';
281
+ target.style['top'] = '220px';
282
+ }
283
+ } catch (error) {
284
+ handleError(error);
285
+ }
286
+ };
287
+ }
288
+
289
+ function isMobile () {
290
+ const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
291
+
292
+ return vw <= 980;
293
+ }
294
+
295
+ function calculateLargerScreenHalf (target) {
296
+ const vh = Math.min(document.documentElement.clientHeight || 0, window.innerHeight || 0);
297
+
298
+ const targetBox = target.getBoundingClientRect();
299
+ const spaceAbove = targetBox.top;
300
+ const spaceBelow = vh - targetBox.bottom;
301
+
302
+ return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
303
+ }
304
+
305
+ async function getLists () {
306
+ return myFtClient.getAll('created', 'list')
307
+ .then(lists => lists.filter(list => !list.isRedirect))
308
+ .then(lists => {
309
+ return lists.map(list => ({ name: list.name, uuid: list.uuid, checked: false }));
310
+ });
311
+ }
312
+
313
+ function triggerAddToListEvent (contentId) {
314
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
315
+ detail: {
316
+ category: 'professorLists',
317
+ action: 'add-to-list',
318
+ article_id: contentId,
319
+ teamName: 'customer-products-us-growth',
320
+ amplitudeExploratory: true
321
+ },
322
+ bubbles: true
323
+ }));
324
+ }
325
+
326
+ function triggerRemoveFromListEvent (contentId) {
327
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
328
+ detail: {
329
+ category: 'professorLists',
330
+ action: 'remove-from-list',
331
+ article_id: contentId,
332
+ teamName: 'customer-products-us-growth',
333
+ amplitudeExploratory: true
334
+ },
335
+ bubbles: true
336
+ }));
337
+ }
338
+
339
+ function triggerCreateListEvent (contentId) {
340
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
341
+ detail: {
342
+ category: 'professorLists',
343
+ action: 'create-list',
344
+ article_id: contentId,
345
+ teamName: 'customer-products-us-growth',
346
+ amplitudeExploratory: true
347
+ },
348
+ bubbles: true
349
+ }));
350
+
351
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
352
+ detail: {
353
+ category: 'myFT',
354
+ action: 'create-list-success',
355
+ article_id: contentId
356
+ },
357
+ bubbles: true
358
+ }));
359
+ }
360
+
361
+ function handleError (error) {
362
+ document.body.dispatchEvent(new CustomEvent('oErrors.log', {
363
+ bubbles: true,
364
+ detail: {
365
+ error,
366
+ info: { component: 'professorLists' },
367
+ }
368
+ }));
369
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@financial-times/n-myft-ui",
3
- "version": "27.1.0",
3
+ "version": "28.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
9
  "prepare": "npx snyk protect || npx snyk protect -d || true",
10
- "preinstall": "npm_config_yes=true npx check-engine"
10
+ "preinstall": "[ \"$INIT_CWD\" != \"$PWD\" ] || npm_config_yes=true npx check-engine"
11
11
  },
12
12
  "repository": {
13
13
  "type": "git",
@@ -20,11 +20,11 @@
20
20
  },
21
21
  "homepage": "https://github.com/Financial-Times/n-myft-ui#readme",
22
22
  "devDependencies": {
23
+ "@financial-times/dotcom-build-base": "^7.0.0",
23
24
  "@financial-times/dotcom-build-bower-resolve": "^2.6.2",
24
25
  "@financial-times/dotcom-build-code-splitting": "^5.0.0",
25
26
  "@financial-times/dotcom-build-js": "^5.0.0",
26
27
  "@financial-times/dotcom-build-sass": "^5.0.0",
27
- "@financial-times/dotcom-page-kit-cli": "^0.6.5",
28
28
  "@financial-times/dotcom-server-handlebars": "^5.0.0",
29
29
  "@financial-times/n-express": "^23.0.1",
30
30
  "@financial-times/n-gage": "^8.3.2",
@@ -75,16 +75,27 @@
75
75
  "mocha": "6.2.2",
76
76
  "mockery": "2.1.0",
77
77
  "node-fetch": "2.6.0",
78
- "node-sass": "^6.0.1",
79
78
  "nodemon": "^1.9.2",
80
79
  "npm-prepublish": "^1.2.1",
81
80
  "pa11y-ci": "^2.1.1",
82
81
  "postcss-loader": "^0.9.1",
83
82
  "regenerator-runtime": "^0.13.3",
83
+ "sass": "^1.51.0",
84
84
  "semver": "6.3.0",
85
85
  "sinon": "^7.1.0",
86
86
  "sinon-chai": "^3.2.0",
87
- "snyk": "^1.216.5"
87
+ "snyk": "^1.216.5",
88
+ "webpack": "^4.46.0",
89
+ "webpack-cli": "^4.9.2"
90
+ },
91
+ "peerDependencies": {
92
+ "n-ui-foundations": "^9.0.0"
93
+ },
94
+ "dependencies": {
95
+ "ftdomdelegate": "^4.0.6",
96
+ "next-myft-client": "^9.0.0",
97
+ "next-session-client": "^4.0.0",
98
+ "superstore-sync": "^2.1.1"
88
99
  },
89
100
  "volta": {
90
101
  "node": "16.14.2",
@@ -0,0 +1,23 @@
1
+ const path = require('path');
2
+ const { PageKitJsPlugin } = require('@financial-times/dotcom-build-js');
3
+ const { PageKitSassPlugin } = require('@financial-times/dotcom-build-sass');
4
+ const { PageKitBowerResolvePlugin } = require('@financial-times/dotcom-build-bower-resolve');
5
+
6
+ module.exports = {
7
+ plugins: [
8
+ new PageKitJsPlugin(),
9
+ new PageKitSassPlugin(),
10
+ new PageKitBowerResolvePlugin()
11
+ ],
12
+ entry: {
13
+ scripts: [
14
+ './myft/index.js',
15
+ './myft-common/index.js'
16
+ ],
17
+
18
+ styles: './myft/test.scss'
19
+ },
20
+ output: {
21
+ path: path.resolve(__dirname, 'public')
22
+ },
23
+ };