@financial-times/qanda-ui 0.0.1-beta.2 → 0.0.1-beta.21

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 (123) hide show
  1. package/README.md +38 -6
  2. package/dist/cjs/components/Author/index.js +19 -0
  3. package/dist/cjs/components/Calendar/Apps.js +48 -0
  4. package/dist/cjs/components/Calendar/CalendarList.js +12 -0
  5. package/dist/cjs/components/Calendar/CalendarListItem.js +24 -0
  6. package/dist/cjs/components/Calendar/index.js +47 -0
  7. package/dist/cjs/components/CountdownTimer/index.js +91 -0
  8. package/dist/cjs/components/EmbedFormField/index.js +24 -0
  9. package/dist/cjs/components/ErrorMessage/humanReadableError.js +74 -0
  10. package/dist/cjs/components/ErrorMessage/index.js +63 -0
  11. package/dist/cjs/components/Expander/index.js +71 -0
  12. package/dist/cjs/components/ExpertDrawer/index.js +36 -0
  13. package/dist/cjs/components/FloatingActionBar/index.js +59 -0
  14. package/dist/cjs/components/Loader/index.js +7 -0
  15. package/dist/cjs/components/MessageBox/index.js +7 -0
  16. package/dist/cjs/components/Overlay/index.js +18 -0
  17. package/dist/cjs/components/Qanda.js +37 -0
  18. package/dist/cjs/components/QandaBlock/index.js +78 -0
  19. package/dist/cjs/components/QandaContainer.js +99 -0
  20. package/dist/cjs/components/QandaProvider.js +11 -0
  21. package/dist/cjs/components/QuestionForm/index.js +165 -0
  22. package/dist/cjs/components/QuestionForm/messagesByUserType.js +33 -0
  23. package/dist/cjs/components/Stream/index.js +37 -0
  24. package/dist/cjs/components/Survey/index.js +36 -0
  25. package/dist/cjs/hooks/useIsMobile.js +15 -0
  26. package/dist/cjs/index.js +53 -0
  27. package/dist/cjs/services/comments-api.js +152 -0
  28. package/dist/cjs/services/error-middleware.js +47 -0
  29. package/dist/cjs/services/reliability.js +79 -0
  30. package/dist/cjs/store/expertSlice.js +26 -0
  31. package/dist/cjs/store/index.js +30 -0
  32. package/dist/cjs/store/questionSlice.js +32 -0
  33. package/dist/cjs/store/stream.js +17 -0
  34. package/dist/cjs/store/updateComments.js +40 -0
  35. package/dist/cjs/store/user.js +51 -0
  36. package/dist/cjs/utils/auth.js +26 -0
  37. package/dist/cjs/utils/qandas.js +20 -0
  38. package/dist/cjs/utils/strings.js +16 -0
  39. package/dist/cjs/utils/tracking.js +11 -0
  40. package/dist/cjs/utils/validate-display-name.js +22 -0
  41. package/dist/esm/components/Author/index.js +14 -0
  42. package/dist/esm/components/Calendar/Apps.js +46 -0
  43. package/dist/esm/components/Calendar/CalendarList.js +7 -0
  44. package/dist/esm/components/Calendar/CalendarListItem.js +22 -0
  45. package/dist/esm/components/Calendar/index.js +42 -0
  46. package/dist/esm/components/CountdownTimer/index.js +88 -0
  47. package/dist/esm/components/EmbedFormField/index.js +22 -0
  48. package/dist/esm/components/ErrorMessage/humanReadableError.js +71 -0
  49. package/dist/esm/components/ErrorMessage/index.js +57 -0
  50. package/dist/esm/components/Expander/index.js +69 -0
  51. package/dist/esm/components/ExpertDrawer/index.js +31 -0
  52. package/dist/esm/components/FloatingActionBar/index.js +57 -0
  53. package/dist/esm/components/Loader/index.js +4 -0
  54. package/dist/esm/components/MessageBox/index.js +5 -0
  55. package/dist/esm/components/Overlay/index.js +13 -0
  56. package/dist/esm/components/Qanda.js +32 -0
  57. package/dist/esm/components/QandaBlock/index.js +73 -0
  58. package/dist/esm/components/QandaContainer.js +94 -0
  59. package/dist/esm/components/QandaProvider.js +8 -0
  60. package/dist/esm/components/QuestionForm/index.js +159 -0
  61. package/dist/esm/components/QuestionForm/messagesByUserType.js +28 -0
  62. package/dist/esm/components/Stream/index.js +32 -0
  63. package/dist/esm/components/Survey/index.js +34 -0
  64. package/dist/esm/hooks/useIsMobile.js +12 -0
  65. package/dist/esm/index.js +46 -0
  66. package/dist/esm/services/comments-api.js +147 -0
  67. package/dist/esm/services/error-middleware.js +43 -0
  68. package/dist/esm/services/reliability.js +75 -0
  69. package/dist/esm/store/expertSlice.js +22 -0
  70. package/dist/esm/store/index.js +24 -0
  71. package/dist/esm/store/questionSlice.js +28 -0
  72. package/dist/esm/store/stream.js +14 -0
  73. package/dist/esm/store/updateComments.js +36 -0
  74. package/dist/esm/store/user.js +45 -0
  75. package/dist/esm/utils/auth.js +24 -0
  76. package/dist/esm/utils/qandas.js +16 -0
  77. package/dist/esm/utils/strings.js +13 -0
  78. package/dist/esm/utils/tracking.js +7 -0
  79. package/dist/esm/utils/validate-display-name.js +19 -0
  80. package/dist/qanda.scss +802 -0
  81. package/dist/types/components/Author/index.d.ts +18 -0
  82. package/dist/types/components/Calendar/Apps.d.ts +14 -0
  83. package/dist/types/components/Calendar/CalendarList.d.ts +2 -0
  84. package/dist/types/components/Calendar/CalendarListItem.d.ts +8 -0
  85. package/dist/types/components/Calendar/index.d.ts +2 -0
  86. package/dist/types/components/CountdownTimer/index.d.ts +6 -0
  87. package/dist/types/components/EmbedFormField/index.d.ts +5 -0
  88. package/dist/types/components/ErrorMessage/humanReadableError.d.ts +29 -0
  89. package/dist/types/components/ErrorMessage/index.d.ts +8 -0
  90. package/dist/types/components/Expander/index.d.ts +8 -0
  91. package/dist/types/components/ExpertDrawer/index.d.ts +2 -0
  92. package/dist/types/components/FloatingActionBar/index.d.ts +4 -0
  93. package/dist/types/components/Loader/index.d.ts +14 -0
  94. package/dist/types/components/MessageBox/index.d.ts +10 -0
  95. package/dist/types/components/Overlay/index.d.ts +12 -0
  96. package/dist/types/components/Qanda.d.ts +3 -0
  97. package/dist/types/components/QandaBlock/index.d.ts +7 -0
  98. package/dist/types/components/QandaContainer.d.ts +6 -0
  99. package/dist/types/components/QandaProvider.d.ts +9 -0
  100. package/dist/types/components/QuestionForm/index.d.ts +1 -0
  101. package/dist/types/components/QuestionForm/messagesByUserType.d.ts +7 -0
  102. package/dist/types/components/Stream/index.d.ts +5 -0
  103. package/dist/types/components/Survey/index.d.ts +6 -0
  104. package/dist/types/hooks/useIsMobile.d.ts +1 -0
  105. package/dist/types/index.d.ts +17 -0
  106. package/dist/types/services/comments-api.d.ts +52 -0
  107. package/dist/types/services/error-middleware.d.ts +2 -0
  108. package/dist/types/services/reliability.d.ts +19 -0
  109. package/dist/types/store/expertSlice.d.ts +14 -0
  110. package/dist/types/store/index.d.ts +218 -0
  111. package/dist/types/store/questionSlice.d.ts +16 -0
  112. package/dist/types/store/stream.d.ts +7 -0
  113. package/dist/types/store/updateComments.d.ts +3 -0
  114. package/dist/types/store/user.d.ts +26 -0
  115. package/dist/types/utils/auth.d.ts +6 -0
  116. package/dist/types/utils/qandas.d.ts +2 -0
  117. package/dist/types/utils/strings.d.ts +1 -0
  118. package/dist/types/utils/tracking.d.ts +2 -0
  119. package/dist/types/utils/validate-display-name.d.ts +1 -0
  120. package/package.json +35 -29
  121. package/dist/index.html +0 -19
  122. package/dist/index.js +0 -2
  123. package/dist/index.js.LICENSE.txt +0 -15
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("preact/jsx-runtime");
4
+ const react_redux_1 = require("react-redux");
5
+ const o3_button_1 = require("@financial-times/o3-button");
6
+ const questionSlice_1 = require("../../store/questionSlice");
7
+ const expertSlice_1 = require("../../store/expertSlice");
8
+ const useIsMobile_1 = require("../../hooks/useIsMobile");
9
+ const comments_api_1 = require("../../services/comments-api");
10
+ const updateComments_1 = require("../../store/updateComments");
11
+ const react_1 = require("react");
12
+ const QandaProvider_1 = require("../QandaProvider");
13
+ function LiveIndicator() {
14
+ return ((0, jsx_runtime_1.jsx)("div", { className: "o-labels-indicator--live o3-type-label floating-action-bar__live-container", children: (0, jsx_runtime_1.jsx)("span", { className: "o-labels-indicator__status floating-action-bar__live-label", children: "Live" }) }));
15
+ }
16
+ function FloatingActionBar({ showLiveIndicator = true, }) {
17
+ const dispatch = (0, react_redux_1.useDispatch)();
18
+ const isMobile = (0, useIsMobile_1.useIsMobile)();
19
+ const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
20
+ const updatedComments = (0, comments_api_1.useUpdatedComments)({
21
+ storyId,
22
+ useStaging,
23
+ commentsAPIUrl,
24
+ });
25
+ const handleShowUpdates = () => {
26
+ (0, updateComments_1.updateComments)(dispatch, updatedComments, storyId, useStaging, commentsAPIUrl);
27
+ // Scrolling to start of qanda blocks container
28
+ const element = document.getElementById('qanda__blocks-container');
29
+ if (element) {
30
+ element.scrollIntoView({ behavior: 'smooth', block: 'start' });
31
+ }
32
+ };
33
+ const showExpertView = (0, react_redux_1.useSelector)((state) => state.expert.expertView);
34
+ const userConfig = {
35
+ descripton: 'Ask a question',
36
+ testIdSuffix: 'ask-a-question-button',
37
+ onClick: () => dispatch((0, questionSlice_1.openForm)()),
38
+ dataTrackable: 'open_question_form',
39
+ };
40
+ const expertConfig = {
41
+ descripton: 'Answer Q&A',
42
+ testIdSuffix: 'answer-a-qanda-button',
43
+ onClick: () => dispatch((0, expertSlice_1.openExpertForm)()),
44
+ dataTrackable: 'open_expert_drawer',
45
+ };
46
+ const view = showExpertView ? expertConfig : userConfig;
47
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar__container", "data-trackable": "floating-action-bar", children: [(0, jsx_runtime_1.jsx)("div", { className: "floating-action-bar__fade-overlay" }), (0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar", "data-testid": "floating-action-bar", children: [showLiveIndicator && (0, jsx_runtime_1.jsx)(LiveIndicator, {}), (0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar__buttons", children: [(0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
48
+ ariaLabel: view.descripton,
49
+ 'data-trackable': view.dataTrackable,
50
+ 'data-trackable-context-action': view.descripton,
51
+ 'data-testid': `floating-action-bar__${view.testIdSuffix}`,
52
+ }, label: view.descripton, type: "ghost", theme: "mono", icon: "edit", iconOnly: isMobile, onClick: view.onClick }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
53
+ ariaLabel: 'Back to top',
54
+ 'data-trackable': 'back_to_top',
55
+ 'data-trackable-context-action': 'Back to top',
56
+ 'data-testid': 'floating-action-bar__back-to-top-button',
57
+ }, label: "Back to top", type: "ghost", theme: "mono", icon: "scroll-to", iconOnly: isMobile, onClick: handleShowUpdates })] }), updatedComments?.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "floating-action-bar__updates-counter o3-type-detail", children: updatedComments.length }))] })] }));
58
+ }
59
+ exports.default = FloatingActionBar;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = Loader;
4
+ const jsx_runtime_1 = require("preact/jsx-runtime");
5
+ function Loader({ size = 'small', theme = 'light', extraClassNames = '', }) {
6
+ return ((0, jsx_runtime_1.jsx)("div", { className: `o-loading o-loading--${theme} o-loading--${size} ${extraClassNames}`, role: "progressbar", "aria-label": "Loading content" }));
7
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("preact/jsx-runtime");
4
+ function MessageBox({ color, extraClassNames = '', text, title, id = '', }) {
5
+ return ((0, jsx_runtime_1.jsx)("div", { class: `o-message o-message--inner message-box__container ${extraClassNames}`, "data-o-message-close": "false", "data-o-component": "o-message", id: id, children: (0, jsx_runtime_1.jsx)("div", { class: "o-message__container", children: (0, jsx_runtime_1.jsxs)("div", { class: "message-box__content", children: [(0, jsx_runtime_1.jsx)("span", { style: { color }, class: "o3-type-body-highlight", children: title }), (0, jsx_runtime_1.jsx)("span", { style: { color }, class: "o3-type-body-base", children: text })] }) }) }));
6
+ }
7
+ exports.default = MessageBox;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const o3_button_1 = require("@financial-times/o3-button");
8
+ const classnames_1 = __importDefault(require("classnames"));
9
+ function Overlay({ title, children, closeCallback, options = {}, }) {
10
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "overlay", children: [(0, jsx_runtime_1.jsx)("div", { className: (0, classnames_1.default)([
11
+ 'overlay__outer',
12
+ { 'overlay__outer--no-background': options.noBackground },
13
+ ]) }), (0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)([
14
+ 'overlay__inner',
15
+ { 'overlay__inner--shadow': options.shadow },
16
+ ]), children: [(0, jsx_runtime_1.jsx)("div", { className: "overlay__header", children: (0, jsx_runtime_1.jsxs)("div", { className: "overlay__header-bar", children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-type-body-highlight overlay__header-title", children: title }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: { className: 'overlay__header-close' }, iconOnly: true, icon: "cross", label: "Close", type: "ghost", theme: "mono", onClick: () => closeCallback() })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "overlay__content", children: children })] })] }));
17
+ }
18
+ exports.default = Overlay;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const react_redux_1 = require("react-redux");
8
+ const store_1 = require("../store");
9
+ const QandaContainer_1 = __importDefault(require("./QandaContainer"));
10
+ const QandaProvider_1 = require("./QandaProvider");
11
+ function Qanda({ storyId, useStaging, title, commentsAPIHost, commentsAPIVersion, maxQuestionLength, embedFormFieldContainer, countdownTimerContainer, }) {
12
+ // check we have a valid ft.com url and remove any trailing slashes
13
+ const allowedHostRegx = /^[\w\-\.]+(\.ft\.com|\.ftweb\.tech)$/i;
14
+ const allowedVersionRegx = /^v\d+$/i;
15
+ const validateApiUrl = (url, version) => {
16
+ const baseURL = new URL(url);
17
+ if (!allowedHostRegx.test(baseURL.hostname) ||
18
+ !allowedVersionRegx.test(version)) {
19
+ console.error('Invalid comments API details provided');
20
+ return;
21
+ }
22
+ return `https://${baseURL.hostname}/qa/${version}`;
23
+ };
24
+ const cleanCommentsAPIUrl = validateApiUrl(commentsAPIHost, commentsAPIVersion);
25
+ if (!storyId || !title || !cleanCommentsAPIUrl) {
26
+ console.error('Missing required props');
27
+ return null;
28
+ }
29
+ return ((0, jsx_runtime_1.jsx)(react_redux_1.Provider, { store: store_1.store, children: (0, jsx_runtime_1.jsx)(QandaProvider_1.QandaContext.Provider, { value: {
30
+ storyId,
31
+ useStaging,
32
+ commentsAPIUrl: cleanCommentsAPIUrl,
33
+ title,
34
+ maxQuestionLength,
35
+ }, children: (0, jsx_runtime_1.jsx)(QandaContainer_1.default, { embedFormFieldContainer: embedFormFieldContainer, countdownTimerContainer: countdownTimerContainer }) }) }));
36
+ }
37
+ exports.default = Qanda;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const Byline_1 = __importDefault(require("@financial-times/cp-content-pipeline-ui/lib/components/Byline"));
8
+ const RichText_1 = __importDefault(require("@financial-times/cp-content-pipeline-ui/lib/components/RichText"));
9
+ require("../Expander/index");
10
+ const index_1 = __importDefault(require("../Expander/index"));
11
+ const Author_1 = __importDefault(require("../Author"));
12
+ const react_1 = require("react");
13
+ function renderTime(publishedDate) {
14
+ const date = new Date(publishedDate);
15
+ return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
16
+ }
17
+ // Get shadow color based on factor
18
+ function interpolateColor(factor, colorStart = 'ccc1b7', colorEnd = '4d4845', alphaStart = 0.5, alphaEnd = 0.85) {
19
+ const hexToRgb = (hex) => hex.match(/[0-9a-f]{2}/g).map((x) => parseInt(x, 16));
20
+ const startRgb = hexToRgb(colorStart);
21
+ const endRgb = hexToRgb(colorEnd);
22
+ const rgb = startRgb.map((val, i) => Math.round(val + (endRgb[i] - val) * factor));
23
+ const alpha = alphaStart + factor * (alphaEnd - alphaStart);
24
+ return `rgba(${rgb.join(', ')}, ${alpha})`;
25
+ }
26
+ function QandaBlock({ id, body, children, byline, subtitle, showAnswerTime = false, }) {
27
+ (0, react_1.useEffect)(() => {
28
+ let currentBubble = null;
29
+ const factorRef = { current: 0 };
30
+ let animationFrameId;
31
+ const observer = new IntersectionObserver((entries) => {
32
+ entries.forEach((entry) => {
33
+ if (entry.isIntersecting) {
34
+ if (currentBubble !== entry.target) {
35
+ factorRef.current = 0;
36
+ }
37
+ currentBubble = entry.target;
38
+ }
39
+ });
40
+ }, {
41
+ root: null,
42
+ rootMargin: '-50% 0px -50% 0px',
43
+ threshold: 0,
44
+ });
45
+ const comments = document.querySelectorAll('.qanda-block__comment');
46
+ comments.forEach((comment) => observer.observe(comment));
47
+ const animate = () => {
48
+ const centerY = window.innerHeight / 2;
49
+ if (currentBubble) {
50
+ const rect = currentBubble.getBoundingClientRect();
51
+ const commentCenterY = rect.top + rect.height / 2;
52
+ const distanceRatio = Math.min(Math.abs(centerY - commentCenterY) / centerY, 1);
53
+ const targetFactor = (1 - distanceRatio) ** 3;
54
+ factorRef.current += (targetFactor - factorRef.current) * 0.1;
55
+ const scale = 1 + 0.04 * factorRef.current;
56
+ const shadow = interpolateColor(factorRef.current);
57
+ currentBubble.style.transform = `scale(${scale})`;
58
+ // NB: drop-shadow blur radius is not calculated the same way as box-shadow blur radius, halving it matches
59
+ currentBubble.style.filter = `drop-shadow(0px 2px 4px ${shadow})`;
60
+ if (factorRef.current < 0.001 && distanceRatio > 0.95) {
61
+ currentBubble.removeAttribute('style');
62
+ currentBubble = null;
63
+ factorRef.current = 0;
64
+ }
65
+ }
66
+ animationFrameId = requestAnimationFrame(animate);
67
+ };
68
+ animationFrameId = requestAnimationFrame(animate);
69
+ window.addEventListener('resize', animate);
70
+ return () => {
71
+ cancelAnimationFrame(animationFrameId);
72
+ window.removeEventListener('resize', animate);
73
+ observer.disconnect();
74
+ };
75
+ }, []);
76
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-block", children: [(0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__comment qanda-block__question", id: id, "data-testid": `qanda-block__question-${id}`, "data-trackable": "liveqa_question", children: [subtitle && ((0, jsx_runtime_1.jsx)("div", { className: "o3-type-headline-sm qanda-block__question-subtitle", children: subtitle })), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: (0, jsx_runtime_1.jsx)(Byline_1.default, { structuredContent: byline, showEditedBy: false }) }), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-base qanda-block__comment-text", children: (0, jsx_runtime_1.jsx)(index_1.default, { className: "qanda-block__text", expandLabel: "Expand question", collapseLabel: "Collapse question", id: id, children: (0, jsx_runtime_1.jsx)(RichText_1.default, { structuredContent: body.structured }) }) })] }, id), children?.map((child) => child.body && ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__comment qanda-block__answer", "data-testid": `qanda-block__answer-${child.id}`, "data-trackable": "liveqa_answer", children: [(0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__answer-header", children: [(0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: child.author ? ((0, jsx_runtime_1.jsx)(Author_1.default, { byline: child.byline, headshot: child.author.headshot, prefLabel: child.author.prefLabel, role: child.author.role, streamPage: child.author.streamPage })) : ((0, jsx_runtime_1.jsx)(Byline_1.default, { structuredContent: child.byline, showEditedBy: false })) }), showAnswerTime && ((0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail", children: renderTime(child.publishedDate) }))] }), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-base qanda-block__comment-text", children: (0, jsx_runtime_1.jsx)(index_1.default, { className: "qanda-block__text", expandLabel: "Expand answer", collapseLabel: "Collapse answer", id: id, children: (0, jsx_runtime_1.jsx)(RichText_1.default, { structuredContent: child.body.structured }) }) })] }, child.id)))] }));
77
+ }
78
+ exports.default = QandaBlock;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const react_1 = require("react");
8
+ const react_redux_1 = require("react-redux");
9
+ const user_1 = require("../store/user");
10
+ const expertSlice_1 = require("../store/expertSlice");
11
+ const QuestionForm_1 = __importDefault(require("./QuestionForm"));
12
+ const Survey_1 = __importDefault(require("./Survey"));
13
+ const ExpertDrawer_1 = __importDefault(require("./ExpertDrawer"));
14
+ const Stream_1 = __importDefault(require("./Stream"));
15
+ const Calendar_1 = __importDefault(require("./Calendar"));
16
+ const CountdownTimer_1 = __importDefault(require("./CountdownTimer"));
17
+ const FloatingActionBar_1 = __importDefault(require("./FloatingActionBar"));
18
+ const EmbedFormField_1 = __importDefault(require("./EmbedFormField"));
19
+ const QandaProvider_1 = require("./QandaProvider");
20
+ const o_tracking_1 = __importDefault(require("@financial-times/o-tracking"));
21
+ const comments_api_1 = require("../services/comments-api");
22
+ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
23
+ const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
24
+ const dispatch = (0, react_redux_1.useDispatch)();
25
+ const [liveMessage, setLiveMessage] = (0, react_1.useState)('');
26
+ const { data, error, isLoading } = (0, comments_api_1.useGetQandAStreamQuery)({
27
+ storyId,
28
+ useStaging,
29
+ commentsAPIUrl,
30
+ status: 'pre-live',
31
+ });
32
+ const [ariaMessage, setAriaMessage] = (0, react_1.useState)('');
33
+ (0, react_1.useEffect)(() => {
34
+ if (isLoading) {
35
+ setAriaMessage('Loading Q&A');
36
+ }
37
+ else if (error) {
38
+ setAriaMessage('Error loading Q&A');
39
+ }
40
+ else if (data) {
41
+ setAriaMessage('Q&A stream has been updated');
42
+ }
43
+ }, [isLoading, error, data]);
44
+ const updatedQA = (0, comments_api_1.useUpdatedQA)({
45
+ storyId,
46
+ useStaging,
47
+ commentsAPIUrl,
48
+ });
49
+ const status = updatedQA?.status || data?.status || 'pre-live';
50
+ const profile = (0, react_redux_1.useSelector)((state) => state.user.profile);
51
+ const { data: roleData } = (0, comments_api_1.useGetUserRolesQuery)({
52
+ storyId,
53
+ useStaging,
54
+ commentsAPIUrl,
55
+ token: profile.token,
56
+ }, { skip: !profile.token });
57
+ const expertView = roleData?.expertView;
58
+ (0, react_1.useEffect)(() => {
59
+ if (expertView !== undefined) {
60
+ dispatch((0, expertSlice_1.setExpertView)(expertView));
61
+ }
62
+ }, [expertView, dispatch]);
63
+ const showExpertView = (0, react_redux_1.useSelector)((state) => state.expert.expertView);
64
+ (0, react_1.useEffect)(() => {
65
+ dispatch((0, user_1.getUserProfile)({ useStaging, commentsAPIUrl }));
66
+ return () => {
67
+ // ensure we reset the api state when the component is unmounted
68
+ dispatch(comments_api_1.nextCommentsApi.util.resetApiState());
69
+ };
70
+ }, [dispatch, storyId]);
71
+ (0, react_1.useEffect)(() => {
72
+ if (!data || !status)
73
+ return;
74
+ o_tracking_1.default.view.init({
75
+ selector: '.qanda-container',
76
+ getContextData: () => {
77
+ return {
78
+ component: {
79
+ name: 'liveqa',
80
+ type: 'container',
81
+ id: '663ab2ca-77ca-4b50-873d-1fd2ff21ff08',
82
+ },
83
+ content: {
84
+ asset_type: 'article',
85
+ uuid: storyId,
86
+ },
87
+ custom: [
88
+ {
89
+ name: 'status',
90
+ value: status,
91
+ },
92
+ ],
93
+ };
94
+ },
95
+ });
96
+ }, [data, status]);
97
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-container", "data-trackable": "liveqa", children: [error && ((0, jsx_runtime_1.jsxs)("p", { children: ["We are not able to load the Q&A at the moment. Please try again later. Error:", error] })), !isLoading && !error && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [status !== 'close' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [showExpertView ? (0, jsx_runtime_1.jsx)(ExpertDrawer_1.default, {}) : (0, jsx_runtime_1.jsx)(QuestionForm_1.default, {}), (0, jsx_runtime_1.jsx)(CountdownTimer_1.default, { container: countdownTimerContainer })] })), status === 'pre-live' && (0, jsx_runtime_1.jsx)(Calendar_1.default, {}), (0, jsx_runtime_1.jsx)(Stream_1.default, { status: status }), (0, jsx_runtime_1.jsx)(Survey_1.default, { storyId: storyId, status: status }), (status === 'live' || (status === 'pre-live' && showExpertView)) && ((0, jsx_runtime_1.jsx)(FloatingActionBar_1.default, { showLiveIndicator: status === 'live' })), status !== 'close' && !showExpertView && ((0, jsx_runtime_1.jsx)(EmbedFormField_1.default, { container: embedFormFieldContainer }))] })), (0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", "aria-atomic": "true", className: "o3-visually-hidden", children: ariaMessage })] }));
98
+ }
99
+ exports.default = QandaContainer;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QandaContext = void 0;
4
+ const react_1 = require("react");
5
+ exports.QandaContext = (0, react_1.createContext)({
6
+ storyId: '',
7
+ useStaging: true,
8
+ commentsAPIUrl: '',
9
+ title: '',
10
+ maxQuestionLength: 0,
11
+ });
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = QuestionForm;
7
+ const jsx_runtime_1 = require("preact/jsx-runtime");
8
+ const react_redux_1 = require("react-redux");
9
+ const classnames_1 = __importDefault(require("classnames"));
10
+ const react_1 = require("react");
11
+ const questionSlice_1 = require("../../store/questionSlice");
12
+ const QandaProvider_1 = require("../QandaProvider");
13
+ const comments_api_1 = require("../../services/comments-api");
14
+ const Overlay_1 = __importDefault(require("../Overlay"));
15
+ const Loader_1 = __importDefault(require("../Loader"));
16
+ const o3_button_1 = require("@financial-times/o3-button");
17
+ const MessageBox_1 = __importDefault(require("../MessageBox"));
18
+ const ErrorMessage_1 = __importDefault(require("../ErrorMessage"));
19
+ const user_1 = require("../../store/user");
20
+ const validate_display_name_1 = __importDefault(require("../../utils/validate-display-name"));
21
+ const messagesByUserType_1 = require("./messagesByUserType");
22
+ const tracking_1 = require("../../utils/tracking");
23
+ function QuestionForm() {
24
+ const dispatch = (0, react_redux_1.useDispatch)();
25
+ const { storyId, useStaging, commentsAPIUrl, maxQuestionLength } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
26
+ const [postAnonymously, setPostAnonymously] = (0, react_1.useState)(false);
27
+ const [newDisplayName, setNewDisplayName] = (0, react_1.useState)(null);
28
+ const [submitEnabled, setSubmitEnabled] = (0, react_1.useState)(false);
29
+ const { isFormOpen, question, displayNameError } = (0, react_redux_1.useSelector)((state) => state.question);
30
+ const { profile, error: userDisplayNameError } = (0, react_redux_1.useSelector)((state) => state.user);
31
+ // NB: reset will have the effect of setting things like error to null and isSuccess to empty
32
+ // you use this when you want to clear the state of the usePostQuestionMutation hook as if you had just started on the page
33
+ // as we allow users to see things like error messages and success messages, we do not clear those until we try to send another mutation, otherwise those things will clear too early
34
+ const [postQuestion, { isLoading, isSuccess, error, reset }] = (0, comments_api_1.usePostQuestionMutation)();
35
+ (0, react_1.useEffect)(() => {
36
+ if (userDisplayNameError) {
37
+ dispatch((0, questionSlice_1.updateDisplayNameError)('Sorry, we are unable to update display names. Please try again.'));
38
+ }
39
+ }, [userDisplayNameError]);
40
+ (0, react_1.useEffect)(() => {
41
+ setSubmitEnabled(Boolean((profile?.token || (newDisplayName && newDisplayName.length > 0)) &&
42
+ question.length >= 20));
43
+ }, [profile?.token, newDisplayName, question]);
44
+ (0, react_1.useEffect)(() => {
45
+ // we want to clear the display name error unless the person has attempted
46
+ // to post anonymously with no display name
47
+ if (!postAnonymously && newDisplayName && newDisplayName?.length > 0) {
48
+ dispatch((0, questionSlice_1.updateDisplayNameError)(null));
49
+ }
50
+ }, [newDisplayName]);
51
+ const handleHideDisplayName = (e) => {
52
+ if (!profile?.token && !newDisplayName) {
53
+ dispatch((0, questionSlice_1.updateDisplayNameError)('You need a display name to ask a question, but you can still post it anonymously.'));
54
+ }
55
+ setPostAnonymously(e.target instanceof HTMLInputElement && e.target.checked);
56
+ };
57
+ const handleSubmit = async (e) => {
58
+ e.preventDefault();
59
+ reset();
60
+ let updatedProfile;
61
+ if (newDisplayName) {
62
+ const invalidCharacters = (0, validate_display_name_1.default)(newDisplayName);
63
+ if (invalidCharacters) {
64
+ dispatch((0, questionSlice_1.updateDisplayNameError)(`The display name contains invalid characters: ${invalidCharacters}`));
65
+ return;
66
+ }
67
+ updatedProfile = await dispatch((0, user_1.getUserProfile)({
68
+ useStaging,
69
+ displayName: newDisplayName,
70
+ commentsAPIUrl,
71
+ })).unwrap();
72
+ // The user token is empty when there is no display name set
73
+ // if there are also no errors, we can assume the display name is taken
74
+ if (!updatedProfile?.token && !userDisplayNameError) {
75
+ dispatch((0, questionSlice_1.updateDisplayNameError)('Unfortunately that display name is taken'));
76
+ return;
77
+ }
78
+ if (updatedProfile?.displayName) {
79
+ // NB: we are using a component act event as analytics team can only support existing categories and actions at this time
80
+ (0, tracking_1.trackEvent)({
81
+ category: 'component',
82
+ action: 'act',
83
+ trigger_action: 'success_set_displayname',
84
+ });
85
+ }
86
+ }
87
+ try {
88
+ await postQuestion({
89
+ token: updatedProfile?.token || profile?.token || '',
90
+ storyId,
91
+ useStaging,
92
+ commentsAPIUrl,
93
+ question: question,
94
+ postAnonymously,
95
+ }).unwrap();
96
+ }
97
+ catch (e) {
98
+ dispatch((0, questionSlice_1.openForm)());
99
+ }
100
+ };
101
+ (0, react_1.useEffect)(() => {
102
+ if (isSuccess) {
103
+ dispatch((0, questionSlice_1.updateQuestion)(''));
104
+ }
105
+ }, [isSuccess]);
106
+ if (!isFormOpen) {
107
+ return;
108
+ }
109
+ return (
110
+ // This form will use o-forms fields with o3 styling until o3 components
111
+ // are implemented by Origami team.
112
+ // Ticket CI-2770 was created to address this in the future.
113
+ (0, jsx_runtime_1.jsxs)(Overlay_1.default, { title: "Ask a question", closeCallback: () => {
114
+ setNewDisplayName(null);
115
+ dispatch((0, questionSlice_1.closeForm)());
116
+ }, children: [profile.isSubscribed && ((0, jsx_runtime_1.jsx)("div", { className: "overlay__content-box overlay__form-box", "data-testid": "question-form-overlay", children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, "data-trackable": `liveqa_submit_question_form_${postAnonymously ? 'anonymous' : 'displayname'}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "overlay__user-container", children: [!postAnonymously && profile?.displayName && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("span", { className: "overlay__user-container-display-name o3-type-body-highlight", children: [(0, jsx_runtime_1.jsx)("i", { className: "overlay__message-icon", style: { maskImage: 'var(--o3-icon-user)' } }), profile.displayName] }) })), !profile?.displayName && ((0, jsx_runtime_1.jsxs)("label", { for: "display-name", className: "question-form__displayname-input o-forms-field", children: [(0, jsx_runtime_1.jsx)("span", { className: "o-forms-title", children: (0, jsx_runtime_1.jsx)("span", { className: "o3-type-body-highlight", children: "Choose a display name" }) }), (0, jsx_runtime_1.jsxs)("span", { className: (0, classnames_1.default)([
117
+ 'question-form__displayname__input',
118
+ 'o-forms-input',
119
+ 'o-forms-input--text',
120
+ { 'o-forms-input--invalid': !!displayNameError },
121
+ ]), children: [(0, jsx_runtime_1.jsx)("input", { className: "question-form__displayname__input__field o3-type-body-lg", id: "display-name", value: newDisplayName || '', onChange: (e) => setNewDisplayName(e.currentTarget.value), type: postAnonymously ? 'password' : 'text', "aria-invalid": displayNameError ? 'true' : 'false', "aria-errormessage": displayNameError ??
122
+ 'question_form__displayname-error-message' }), displayNameError && ((0, jsx_runtime_1.jsx)("span", { className: "question-form__displayname__input__error o3-type-detail", id: "question_form__displayname-error-message", children: (0, jsx_runtime_1.jsx)("span", { children: displayNameError }) }))] })] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "o-forms-field", children: (0, jsx_runtime_1.jsxs)("label", { className: "o-forms-input o-forms-input--checkbox", htmlFor: "post-anonymously", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", name: "post-anonymously", id: "post-anonymously", value: "true", onChange: handleHideDisplayName, "data-trackable-context-action": `Post anonymously ${postAnonymously ? 'off' : 'on'}`, "data-trackable": `question-form__post_anonymously-${postAnonymously ? 'off' : 'on'}` }), (0, jsx_runtime_1.jsx)("span", { className: "o-forms-input__label", children: "Post anonymously (optional)" })] }) }), (0, jsx_runtime_1.jsxs)("label", { for: "question-text", class: "o-forms-field question-form__question__input", children: [(0, jsx_runtime_1.jsx)("span", { class: "o-forms-title", children: (0, jsx_runtime_1.jsx)("span", { class: "o3-type-body-highlight", children: "Enter your question" }) }), (0, jsx_runtime_1.jsx)("span", { class: "o-forms-input o-forms-input--textarea", children: (0, jsx_runtime_1.jsx)("textarea", { id: "question-text", name: "question", value: question, onChange: (e) => dispatch((0, questionSlice_1.updateQuestion)(e.currentTarget.value)), "aria-label": "Your question", placeholder: "Type your question", maxLength: maxQuestionLength, "aria-invalid": error ? 'true' : undefined, "aria-errormessage": "question-form-error-message" }) })] }), (0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail", children: "Questions must be at least 20 characters long." }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
123
+ className: 'overlay__submit-button',
124
+ type: 'submit',
125
+ disabled: !submitEnabled,
126
+ ariaLabel: question.length < 20
127
+ ? 'Submit button (disabled until question is at least 20 characters)'
128
+ : isLoading
129
+ ? 'Submitting...'
130
+ : 'Submit your question',
131
+ },
132
+ // @ts-expect-error - o3 Button is currently accepting a JSX element as label
133
+ // but the type definition needs to be updated by Origami team
134
+ label: isLoading ? (0, jsx_runtime_1.jsx)(Loader_1.default, {}) : 'Submit your question', type: "primary", theme: "mono" }), !profile?.token && ((0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail", children: "Submitting a question will create a community profile with FT." }))] }) })), isSuccess && ((0, jsx_runtime_1.jsx)(MessageBox_1.default, { color: "#00572C", extraClassNames: "o-message--alert o-message--success", title: "Question sent", text: "Your question is in the queue and will be reviewed by our editors." })), error && ((0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error, storyId: storyId, errorMapping: {
135
+ // Messages for specific error codes
136
+ REPEAT_POST: {
137
+ title: 'Question already submitted',
138
+ message: "You've already submitted this question. Try asking something different and submit again.",
139
+ },
140
+ REJECTED: {
141
+ title: 'Question rejected',
142
+ message: 'This question has been rejected for language that violates our guidelines.',
143
+ },
144
+ RATE_LIMIT_EXCEEDED: {
145
+ title: 'Too many questions',
146
+ message: 'You are submitting too many questions in a short amount of time. Please wait sometime before submitting another question.',
147
+ },
148
+ TOXIC_COMMENT: {
149
+ title: 'Question blocked',
150
+ message: 'Are you sure? The language in your question might violate our community guidelines. You can edit it or submit it for moderator review.',
151
+ },
152
+ serverError: {
153
+ title: 'Server error',
154
+ message: 'There was a server error. Please try submitting your question again.',
155
+ },
156
+ connectionError: {
157
+ title: 'Connection error',
158
+ message: "You can't submit your question. Check your internet connection or browser settings and try again.",
159
+ },
160
+ timeoutError: {
161
+ title: 'Timeout error',
162
+ message: 'The request timed out. Please try submitting your question again.',
163
+ },
164
+ }, operation: "submit-question", id: "question-form-error-message" })), (0, jsx_runtime_1.jsx)(MessageBox_1.default, { extraClassNames: "o-message--notice o-message--inform", title: (0, messagesByUserType_1.getTitleByUser)(profile), text: (0, messagesByUserType_1.getTextByUser)(profile) })] }));
165
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TERMS_AND_CONDITIONS_URL = exports.COMMUNITY_GUIDELINES_URL = exports.SUBSCRIBE_URL = exports.SIGN_IN_URL = void 0;
4
+ exports.getTitleByUser = getTitleByUser;
5
+ exports.getTextByUser = getTextByUser;
6
+ const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ exports.SIGN_IN_URL = 'https://www.ft.com/login';
8
+ exports.SUBSCRIBE_URL = 'https://subs.ft.com/products';
9
+ exports.COMMUNITY_GUIDELINES_URL = 'https://help.ft.com/help/article-queries/commenting-on-an-article/commenting-guidelines';
10
+ exports.TERMS_AND_CONDITIONS_URL = 'https://help.ft.com/help/legal-privacy/terms-conditions';
11
+ function getTitleByUser(profile) {
12
+ if (profile.isSubscribed) {
13
+ return 'Q&A guidelines';
14
+ }
15
+ if (profile.isTrialist) {
16
+ return 'You are still on a trial period';
17
+ }
18
+ // Same title for anonymous and registered users
19
+ return 'Asking a question is only available to readers with an FT subscription';
20
+ }
21
+ function getTextByUser(profile) {
22
+ if (profile.isSubscribed) {
23
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Keep your comments respectful and easy to understand for our global audience. By submitting, you agree to follow our", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": "question-form__community-guidelines", "data-trackable-context-action": "Community guidelines", href: exports.COMMUNITY_GUIDELINES_URL, target: "_parent", children: "community guidelines" }), ' ', "and", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": "question-form__terms-and-conditions", "data-trackable-context-action": "Terms and conditions", href: exports.TERMS_AND_CONDITIONS_URL, target: "_parent", children: "terms" }), "."] }));
24
+ }
25
+ if (profile.isTrialist) {
26
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["View our full", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": "question-form__subscribe-trialist", "data-trackable-context-action": "Subscribe", href: exports.SUBSCRIBE_URL, target: "_parent", children: "subscription packages" }), ' ', "to join the conversation."] }));
27
+ }
28
+ if (profile.isRegistered) {
29
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": "question-form__subscribe-registered", "data-trackable-context-action": "Subscribe", href: exports.SUBSCRIBE_URL, target: "_parent", children: "Subscribe" }), ' ', "to join the conversation."] }));
30
+ }
31
+ // Anonymous users
32
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Please", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": "question-form__sign-in-anonymous", "data-trackable-context-action": "Sign in", href: exports.SIGN_IN_URL, target: "_parent", children: "sign in" }), ' ', "or", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": "question-form__subscribe-anonymous", "data-trackable-context-action": "Subscribe", href: exports.SUBSCRIBE_URL, target: "_parent", children: "subscribe" }), ' ', "to join the conversation."] }));
33
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const react_1 = require("react");
8
+ const react_redux_1 = require("react-redux");
9
+ const index_1 = __importDefault(require("../QandaBlock/index"));
10
+ const QandaProvider_1 = require("../QandaProvider");
11
+ const Loader_1 = __importDefault(require("../Loader"));
12
+ const comments_api_1 = require("../../services/comments-api");
13
+ const ErrorMessage_1 = __importDefault(require("../ErrorMessage"));
14
+ function Stream({ status = 'pre-live' }) {
15
+ const latestAnsweredQuestionId = (0, react_redux_1.useSelector)((state) => state.stream.latestAnsweredQuestionId);
16
+ const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
17
+ const { data, error, isLoading } = (0, comments_api_1.useGetQandAStreamQuery)({
18
+ storyId,
19
+ useStaging,
20
+ commentsAPIUrl,
21
+ status,
22
+ },
23
+ // In comments api, the stream endpoint uses different queries based on the Q&A status (live stream vs pre-live stream),
24
+ // If the status changes and Qanda receives an update,
25
+ // we need to force RTK to re-fetch the data and show the appropriate stream.
26
+ { refetchOnMountOrArgChange: true });
27
+ (0, react_1.useEffect)(() => {
28
+ const element = document.getElementById(latestAnsweredQuestionId);
29
+ element?.scrollIntoView({ behavior: 'smooth' });
30
+ }, [latestAnsweredQuestionId]);
31
+ return ((0, jsx_runtime_1.jsxs)("section", { className: "qanda__stream", children: [(0, jsx_runtime_1.jsx)("h2", { className: "o3-type-title-md qanda__stream-title", children: data && data.status && data.status === 'pre-live'
32
+ ? 'Top questions so far'
33
+ : 'What our readers are asking' }), (0, jsx_runtime_1.jsxs)("div", { className: "qanda__blocks-container", "data-testid": "qanda__blocks-container", id: "qanda__blocks-container", "aria-busy": isLoading, children: [isLoading && ((0, jsx_runtime_1.jsx)(Loader_1.default, { theme: "dark", extraClassNames: "qanda__stream-loader" })), error && ((0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error, storyId: storyId, operation: "loading-stream" })), data &&
34
+ data.qandas &&
35
+ data.qandas.map((qandaBlock) => ((0, jsx_runtime_1.jsx)(index_1.default, { id: qandaBlock.id, body: qandaBlock.body, type: "comment", publishedDate: qandaBlock.publishedDate, author: qandaBlock.author, byline: qandaBlock.byline, showAnswerTime: data.status === 'live', children: qandaBlock.children }, qandaBlock.id)))] })] }));
36
+ }
37
+ exports.default = Stream;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("preact/jsx-runtime");
4
+ function Survey({ storyId, status }) {
5
+ const trackEvent = (trackingData) => {
6
+ const event = new CustomEvent('oTracking.event', {
7
+ detail: trackingData,
8
+ bubbles: true,
9
+ });
10
+ document.body.dispatchEvent(event);
11
+ };
12
+ const handleClick = () => {
13
+ trackEvent({
14
+ category: 'component',
15
+ component: {
16
+ name: 'liveqa',
17
+ type: 'container',
18
+ id: '663ab2ca-77ca-4b50-873d-1fd2ff21ff08',
19
+ },
20
+ action: 'act',
21
+ trigger_action: 'liveqa_survey_open',
22
+ content: {
23
+ asset_type: 'article',
24
+ uuid: storyId,
25
+ },
26
+ custom: [
27
+ {
28
+ name: 'status',
29
+ value: status,
30
+ },
31
+ ],
32
+ });
33
+ };
34
+ return ((0, jsx_runtime_1.jsx)("div", { className: "o-message o-message--action o-message--inform o-message--survey", style: { marginBottom: 'var(--o3-spacing-l)' }, "data-o-message-close": "false", "data-o-component": "o-message", children: (0, jsx_runtime_1.jsx)("div", { className: "o-message__container", children: (0, jsx_runtime_1.jsxs)("div", { className: "o-message__content", children: [(0, jsx_runtime_1.jsx)("p", { className: "o-message__content-main", style: { fontSize: 'var(--o3-type-body-base-font-size)' }, children: "How was your experience?" }), (0, jsx_runtime_1.jsx)("div", { className: "o-message__actions", children: (0, jsx_runtime_1.jsx)("a", { href: `https://www.feedback.ft.com/c/a/6HTDVvrHSyaKLZ5281McAo?content_id=${storyId}&qa_status=${status}`, onClick: handleClick, className: "o-message__actions__primary", "data-trackable": "liveqa_survey_link", children: "Share your feedback" }) })] }) }) }));
35
+ }
36
+ exports.default = Survey;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useIsMobile = useIsMobile;
4
+ const react_1 = require("react");
5
+ function useIsMobile(breakpoint = 740) {
6
+ const [isMobile, setIsMobile] = (0, react_1.useState)(false);
7
+ (0, react_1.useEffect)(() => {
8
+ const mediaQuery = window.matchMedia(`(max-width: ${breakpoint}px)`);
9
+ const update = () => setIsMobile(mediaQuery.matches);
10
+ update();
11
+ mediaQuery.addEventListener('change', update);
12
+ return () => mediaQuery.removeEventListener('change', update);
13
+ }, [breakpoint]);
14
+ return isMobile;
15
+ }