@financial-times/qanda-ui 0.0.1-beta.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/components/Author/index.js +2 -2
  3. package/dist/cjs/components/Calendar/Apps.js +34 -16
  4. package/dist/cjs/components/Calendar/CalendarListItem.js +5 -1
  5. package/dist/cjs/components/Calendar/index.js +7 -2
  6. package/dist/cjs/components/CountdownTimer/index.js +5 -1
  7. package/dist/cjs/components/EmbedFormField/index.js +15 -6
  8. package/dist/cjs/components/ErrorMessage/humanReadableError.js +81 -0
  9. package/dist/cjs/components/ErrorMessage/index.js +60 -0
  10. package/dist/cjs/components/Expander/index.js +16 -3
  11. package/dist/cjs/components/ExpertDrawer/index.js +8 -3
  12. package/dist/cjs/components/FloatingActionBar/index.js +54 -15
  13. package/dist/cjs/components/MessageBox/index.js +7 -0
  14. package/dist/cjs/components/Overlay/index.js +19 -5
  15. package/dist/cjs/components/Qanda.js +1 -7
  16. package/dist/cjs/components/QandaBlock/index.js +7 -4
  17. package/dist/cjs/components/QandaContainer.js +18 -3
  18. package/dist/cjs/components/QandaProvider.js +31 -1
  19. package/dist/cjs/components/QuestionForm/index.js +83 -35
  20. package/dist/cjs/components/QuestionForm/messagesByUserType.js +7 -5
  21. package/dist/cjs/components/Stream/index.js +32 -5
  22. package/dist/cjs/components/Survey/index.js +8 -0
  23. package/dist/cjs/config/qanda.js +6 -0
  24. package/dist/cjs/index.js +33 -4
  25. package/dist/cjs/services/comments-api.js +72 -12
  26. package/dist/cjs/services/listeners-middleware.js +170 -0
  27. package/dist/cjs/store/index.js +2 -0
  28. package/dist/cjs/store/stream.js +20 -2
  29. package/dist/cjs/store/updateComments.js +9 -4
  30. package/dist/cjs/utils/auth.js +3 -1
  31. package/dist/cjs/utils/strings.js +16 -0
  32. package/dist/cjs/utils/tracking.js +34 -0
  33. package/dist/esm/components/Author/index.js +1 -1
  34. package/dist/esm/components/Calendar/Apps.js +34 -16
  35. package/dist/esm/components/Calendar/CalendarListItem.js +5 -1
  36. package/dist/esm/components/Calendar/index.js +7 -2
  37. package/dist/esm/components/CountdownTimer/index.js +6 -2
  38. package/dist/esm/components/EmbedFormField/index.js +14 -8
  39. package/dist/esm/components/ErrorMessage/humanReadableError.js +78 -0
  40. package/dist/esm/components/ErrorMessage/index.js +54 -0
  41. package/dist/esm/components/Expander/index.js +16 -3
  42. package/dist/esm/components/ExpertDrawer/index.js +8 -3
  43. package/dist/esm/components/FloatingActionBar/index.js +55 -16
  44. package/dist/esm/components/MessageBox/index.js +5 -0
  45. package/dist/esm/components/Overlay/index.js +19 -5
  46. package/dist/esm/components/Qanda.js +2 -8
  47. package/dist/esm/components/QandaBlock/index.js +7 -4
  48. package/dist/esm/components/QandaContainer.js +20 -5
  49. package/dist/esm/components/QandaProvider.js +30 -1
  50. package/dist/esm/components/QuestionForm/index.js +82 -34
  51. package/dist/esm/components/QuestionForm/messagesByUserType.js +7 -5
  52. package/dist/esm/components/Stream/index.js +32 -5
  53. package/dist/esm/components/Survey/index.js +6 -0
  54. package/dist/esm/config/qanda.js +3 -0
  55. package/dist/esm/index.js +33 -4
  56. package/dist/esm/services/comments-api.js +71 -11
  57. package/dist/esm/services/listeners-middleware.js +167 -0
  58. package/dist/esm/store/expertSlice.js +1 -1
  59. package/dist/esm/store/index.js +2 -0
  60. package/dist/esm/store/questionSlice.js +1 -1
  61. package/dist/esm/store/stream.js +18 -1
  62. package/dist/esm/store/updateComments.js +9 -4
  63. package/dist/esm/utils/auth.js +3 -1
  64. package/dist/esm/utils/strings.js +13 -0
  65. package/dist/esm/utils/tracking.js +29 -0
  66. package/dist/qanda.scss +125 -43
  67. package/dist/types/components/Author/index.d.ts +1 -1
  68. package/dist/types/components/Calendar/Apps.d.ts +6 -2
  69. package/dist/types/components/Calendar/CalendarList.d.ts +1 -1
  70. package/dist/types/components/Calendar/CalendarListItem.d.ts +1 -1
  71. package/dist/types/components/EmbedFormField/index.d.ts +3 -1
  72. package/dist/types/components/ErrorMessage/humanReadableError.d.ts +29 -0
  73. package/dist/types/components/ErrorMessage/index.d.ts +8 -0
  74. package/dist/types/components/FloatingActionBar/index.d.ts +3 -1
  75. package/dist/types/components/Loader/index.d.ts +1 -1
  76. package/dist/types/components/{Overlay/MessageBox.d.ts → MessageBox/index.d.ts} +3 -2
  77. package/dist/types/components/Overlay/index.d.ts +3 -3
  78. package/dist/types/components/Qanda.d.ts +1 -1
  79. package/dist/types/components/QandaBlock/index.d.ts +1 -1
  80. package/dist/types/components/QandaProvider.d.ts +9 -1
  81. package/dist/types/components/QuestionForm/messagesByUserType.d.ts +2 -2
  82. package/dist/types/components/Stream/index.d.ts +4 -1
  83. package/dist/types/components/Survey/index.d.ts +7 -0
  84. package/dist/types/config/qanda.d.ts +1 -0
  85. package/dist/types/index.d.ts +2 -0
  86. package/dist/types/services/comments-api.d.ts +30 -598
  87. package/dist/types/services/error-middleware.d.ts +2 -3
  88. package/dist/types/services/listeners-middleware.d.ts +11 -0
  89. package/dist/types/store/expertSlice.d.ts +9 -10
  90. package/dist/types/store/index.d.ts +72 -183
  91. package/dist/types/store/questionSlice.d.ts +9 -8
  92. package/dist/types/store/stream.d.ts +14 -7
  93. package/dist/types/store/user.d.ts +3 -2
  94. package/dist/types/utils/strings.d.ts +1 -0
  95. package/dist/types/utils/tracking.d.ts +4 -0
  96. package/package.json +18 -18
  97. package/dist/cjs/components/Overlay/MessageBox.js +0 -7
  98. package/dist/cjs/components/UserInfo.js +0 -22
  99. package/dist/esm/components/Overlay/MessageBox.js +0 -5
  100. package/dist/esm/components/UserInfo.js +0 -20
  101. package/dist/types/components/UserInfo.d.ts +0 -2
@@ -4,15 +4,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const react_1 = require("react");
7
8
  const o3_button_1 = require("@financial-times/o3-button");
8
9
  const classnames_1 = __importDefault(require("classnames"));
9
10
  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)([
11
+ // Trap keyboard focus within the overlay for accessibility
12
+ const trapFocus = (event) => {
13
+ if (event.key !== 'Tab')
14
+ return;
15
+ if (event.target === endFocusBtn.current) {
16
+ titleRef.current?.focus();
17
+ }
18
+ else if (event.target === titleRef.current && event.shiftKey) {
19
+ endFocusBtn.current?.focus();
20
+ }
21
+ };
22
+ const titleRef = (0, react_1.useRef)(null);
23
+ const endFocusBtn = (0, react_1.useRef)(null);
24
+ (0, react_1.useEffect)(() => {
25
+ titleRef.current?.focus();
26
+ }, []);
27
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "overlay", role: "dialog", "aria-modal": "true", "aria-labelledby": "overlay-title", children: [!options.allowInteraction && (0, jsx_runtime_1.jsx)("div", { className: "overlay__outer" }), (0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)([
14
28
  'overlay__inner',
15
29
  { '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 })] })] }));
30
+ ]), 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", id: "overlay-title", tabIndex: 0, ref: titleRef, onKeyDown: trapFocus, 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 })] }), (0, jsx_runtime_1.jsx)("button", { className: "o3-visually-hidden", "aria-label": "Close", onClick: () => closeCallback(), ref: endFocusBtn, onKeyDown: trapFocus, type: "button", children: "Close" })] }));
17
31
  }
18
32
  exports.default = Overlay;
@@ -26,12 +26,6 @@ function Qanda({ storyId, useStaging, title, commentsAPIHost, commentsAPIVersion
26
26
  console.error('Missing required props');
27
27
  return null;
28
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 }) }) }));
29
+ return ((0, jsx_runtime_1.jsx)(react_redux_1.Provider, { store: store_1.store, children: (0, jsx_runtime_1.jsx)(QandaProvider_1.QandaProvider, { storyId: storyId, useStaging: useStaging, commentsAPIUrl: cleanCommentsAPIUrl, title: title, maxQuestionLength: maxQuestionLength, children: (0, jsx_runtime_1.jsx)(QandaContainer_1.default, { embedFormFieldContainer: embedFormFieldContainer, countdownTimerContainer: countdownTimerContainer }) }) }));
36
30
  }
37
31
  exports.default = Qanda;
@@ -4,11 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const jsx_runtime_1 = require("preact/jsx-runtime");
7
- const cp_content_pipeline_ui_1 = require("@financial-times/cp-content-pipeline-ui");
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"));
8
9
  require("../Expander/index");
9
10
  const index_1 = __importDefault(require("../Expander/index"));
10
11
  const Author_1 = __importDefault(require("../Author"));
11
12
  const react_1 = require("react");
13
+ const tracking_1 = require("../../utils/tracking");
12
14
  function renderTime(publishedDate) {
13
15
  const date = new Date(publishedDate);
14
16
  return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
@@ -22,7 +24,7 @@ function interpolateColor(factor, colorStart = 'ccc1b7', colorEnd = '4d4845', al
22
24
  const alpha = alphaStart + factor * (alphaEnd - alphaStart);
23
25
  return `rgba(${rgb.join(', ')}, ${alpha})`;
24
26
  }
25
- function QandaBlock({ id, body, children, byline, subtitle, showAnswerTime = false, }) {
27
+ function QandaBlock({ id, body, children, byline, summary, showAnswerTime = false, }) {
26
28
  (0, react_1.useEffect)(() => {
27
29
  let currentBubble = null;
28
30
  const factorRef = { current: 0 };
@@ -54,7 +56,8 @@ function QandaBlock({ id, body, children, byline, subtitle, showAnswerTime = fal
54
56
  const scale = 1 + 0.04 * factorRef.current;
55
57
  const shadow = interpolateColor(factorRef.current);
56
58
  currentBubble.style.transform = `scale(${scale})`;
57
- currentBubble.style.filter = `drop-shadow(0px 2px 8px ${shadow})`;
59
+ // NB: drop-shadow blur radius is not calculated the same way as box-shadow blur radius, halving it matches
60
+ currentBubble.style.filter = `drop-shadow(0px 2px 4px ${shadow})`;
58
61
  if (factorRef.current < 0.001 && distanceRatio > 0.95) {
59
62
  currentBubble.removeAttribute('style');
60
63
  currentBubble = null;
@@ -71,6 +74,6 @@ function QandaBlock({ id, body, children, byline, subtitle, showAnswerTime = fal
71
74
  observer.disconnect();
72
75
  };
73
76
  }, []);
74
- 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)(cp_content_pipeline_ui_1.Byline, { 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)(cp_content_pipeline_ui_1.RichText, { 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)(cp_content_pipeline_ui_1.Byline, { 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)(cp_content_pipeline_ui_1.RichText, { structuredContent: child.body.structured }) }) })] }, child.id)))] }));
77
+ 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": (0, tracking_1.trackingNS)('question'), children: [summary && ((0, jsx_runtime_1.jsx)("div", { className: "o3-type-headline-sm qanda-block__question-summary", children: summary })), (0, jsx_runtime_1.jsxs)("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-visually-hidden", children: "Question by " }), (0, jsx_runtime_1.jsx)(Byline_1.default, { structuredContent: byline, showEditedBy: false })] }), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-content-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": (0, tracking_1.trackingNS)('answer'), children: [(0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__answer-header", children: [(0, jsx_runtime_1.jsxs)("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-visually-hidden", children: "Reply from " }), 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.jsxs)("span", { className: "o3-type-detail", children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-visually-hidden", children: "Published at " }), renderTime(child.publishedDate)] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-content-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)))] }));
75
78
  }
76
79
  exports.default = QandaBlock;
@@ -9,15 +9,17 @@ const react_redux_1 = require("react-redux");
9
9
  const user_1 = require("../store/user");
10
10
  const expertSlice_1 = require("../store/expertSlice");
11
11
  const QuestionForm_1 = __importDefault(require("./QuestionForm"));
12
- const UserInfo_1 = __importDefault(require("./UserInfo"));
12
+ const Survey_1 = __importDefault(require("./Survey"));
13
13
  const ExpertDrawer_1 = __importDefault(require("./ExpertDrawer"));
14
14
  const Stream_1 = __importDefault(require("./Stream"));
15
15
  const Calendar_1 = __importDefault(require("./Calendar"));
16
16
  const CountdownTimer_1 = __importDefault(require("./CountdownTimer"));
17
17
  const FloatingActionBar_1 = __importDefault(require("./FloatingActionBar"));
18
18
  const EmbedFormField_1 = __importDefault(require("./EmbedFormField"));
19
+ const ErrorMessage_1 = __importDefault(require("./ErrorMessage"));
19
20
  const QandaProvider_1 = require("./QandaProvider");
20
21
  const comments_api_1 = require("../services/comments-api");
22
+ const qanda_1 = require("../config/qanda");
21
23
  function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
22
24
  const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
23
25
  const dispatch = (0, react_redux_1.useDispatch)();
@@ -25,7 +27,20 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
25
27
  storyId,
26
28
  useStaging,
27
29
  commentsAPIUrl,
30
+ status: 'pre-live',
28
31
  });
32
+ const [ariaMessage, setAriaMessage] = (0, react_1.useState)('');
33
+ (0, react_1.useEffect)(() => {
34
+ if (isLoading) {
35
+ setAriaMessage(`Loading ${qanda_1.PRODUCT_NAME}`);
36
+ }
37
+ else if (error) {
38
+ setAriaMessage(`Error loading ${qanda_1.PRODUCT_NAME}`);
39
+ }
40
+ else if (data) {
41
+ setAriaMessage(`${qanda_1.PRODUCT_NAME} stream has been updated`);
42
+ }
43
+ }, [isLoading, error, data]);
29
44
  const updatedQA = (0, comments_api_1.useUpdatedQA)({
30
45
  storyId,
31
46
  useStaging,
@@ -33,7 +48,7 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
33
48
  });
34
49
  const status = updatedQA?.status || data?.status || 'pre-live';
35
50
  const profile = (0, react_redux_1.useSelector)((state) => state.user.profile);
36
- const { data: roleData } = (0, comments_api_1.useGetUserRolesQuery)({
51
+ const { data: roleData, error: roleError } = (0, comments_api_1.useGetUserRolesQuery)({
37
52
  storyId,
38
53
  useStaging,
39
54
  commentsAPIUrl,
@@ -53,6 +68,6 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
53
68
  dispatch(comments_api_1.nextCommentsApi.util.resetApiState());
54
69
  };
55
70
  }, [dispatch, storyId]);
56
- 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 !== 'close' && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(UserInfo_1.default, {}) })), data?.status === 'live' && (0, jsx_runtime_1.jsx)(FloatingActionBar_1.default, {}), (0, jsx_runtime_1.jsx)(EmbedFormField_1.default, { container: embedFormFieldContainer })] }))] }));
71
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-container", children: [error && ((0, jsx_runtime_1.jsx)("div", { style: { marginBottom: '16px' }, children: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error, storyId: storyId, operation: "stream-load" }) })), roleError && !error && ((0, jsx_runtime_1.jsx)("div", { style: { marginBottom: '16px' }, children: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: roleError, storyId: storyId, operation: "profile-load" }) })), !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, { isLive: status === 'live' })), status !== 'close' && !showExpertView && ((0, jsx_runtime_1.jsx)(EmbedFormField_1.default, { container: embedFormFieldContainer, status: status }))] })), (0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", "aria-atomic": "true", className: "o3-visually-hidden", children: ariaMessage })] }));
57
72
  }
58
73
  exports.default = QandaContainer;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.QandaContext = void 0;
3
+ exports.QandaProvider = exports.QandaContext = void 0;
4
+ const jsx_runtime_1 = require("preact/jsx-runtime");
4
5
  const react_1 = require("react");
5
6
  exports.QandaContext = (0, react_1.createContext)({
6
7
  storyId: '',
@@ -9,3 +10,32 @@ exports.QandaContext = (0, react_1.createContext)({
9
10
  title: '',
10
11
  maxQuestionLength: 0,
11
12
  });
13
+ const QandaProvider = ({ children, storyId, useStaging, commentsAPIUrl, title, maxQuestionLength = 0, }) => {
14
+ const dialogOpenerRef = (0, react_1.useRef)(null);
15
+ const setDialogOpener = (0, react_1.useCallback)((opener) => {
16
+ dialogOpenerRef.current = opener;
17
+ }, []);
18
+ const focusDialogOpener = (0, react_1.useCallback)(() => {
19
+ dialogOpenerRef.current?.focus();
20
+ dialogOpenerRef.current = null;
21
+ }, []);
22
+ const contextValues = (0, react_1.useMemo)(() => ({
23
+ storyId,
24
+ useStaging,
25
+ commentsAPIUrl,
26
+ title,
27
+ maxQuestionLength,
28
+ setDialogOpener,
29
+ focusDialogOpener,
30
+ }), [
31
+ storyId,
32
+ useStaging,
33
+ commentsAPIUrl,
34
+ title,
35
+ maxQuestionLength,
36
+ setDialogOpener,
37
+ focusDialogOpener,
38
+ ]);
39
+ return ((0, jsx_runtime_1.jsx)(exports.QandaContext.Provider, { value: contextValues, children: children }));
40
+ };
41
+ exports.QandaProvider = QandaProvider;
@@ -7,6 +7,7 @@ exports.default = QuestionForm;
7
7
  const jsx_runtime_1 = require("preact/jsx-runtime");
8
8
  const react_redux_1 = require("react-redux");
9
9
  const classnames_1 = __importDefault(require("classnames"));
10
+ const o_tracking_1 = __importDefault(require("@financial-times/o-tracking"));
10
11
  const react_1 = require("react");
11
12
  const questionSlice_1 = require("../../store/questionSlice");
12
13
  const QandaProvider_1 = require("../QandaProvider");
@@ -14,22 +15,39 @@ const comments_api_1 = require("../../services/comments-api");
14
15
  const Overlay_1 = __importDefault(require("../Overlay"));
15
16
  const Loader_1 = __importDefault(require("../Loader"));
16
17
  const o3_button_1 = require("@financial-times/o3-button");
17
- const MessageBox_1 = __importDefault(require("../Overlay/MessageBox"));
18
+ const MessageBox_1 = __importDefault(require("../MessageBox"));
19
+ const ErrorMessage_1 = __importDefault(require("../ErrorMessage"));
18
20
  const user_1 = require("../../store/user");
19
21
  const validate_display_name_1 = __importDefault(require("../../utils/validate-display-name"));
20
22
  const messagesByUserType_1 = require("./messagesByUserType");
23
+ const tracking_1 = require("../../utils/tracking");
21
24
  function QuestionForm() {
22
25
  const dispatch = (0, react_redux_1.useDispatch)();
23
- const { storyId, useStaging, commentsAPIUrl, maxQuestionLength } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
26
+ const { storyId, useStaging, commentsAPIUrl, maxQuestionLength, focusDialogOpener, } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
24
27
  const [postAnonymously, setPostAnonymously] = (0, react_1.useState)(false);
25
28
  const [newDisplayName, setNewDisplayName] = (0, react_1.useState)(null);
26
29
  const [submitEnabled, setSubmitEnabled] = (0, react_1.useState)(false);
27
30
  const { isFormOpen, question, displayNameError } = (0, react_redux_1.useSelector)((state) => state.question);
28
31
  const { profile, error: userDisplayNameError } = (0, react_redux_1.useSelector)((state) => state.user);
32
+ const MIN_QUESTION_LENGTH = 20;
33
+ const MAX_QUESTION_LENGTH = maxQuestionLength || 300;
29
34
  // NB: reset will have the effect of setting things like error to null and isSuccess to empty
30
35
  // you use this when you want to clear the state of the usePostQuestionMutation hook as if you had just started on the page
31
36
  // 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
32
37
  const [postQuestion, { isLoading, isSuccess, error, reset }] = (0, comments_api_1.usePostQuestionMutation)();
38
+ (0, react_1.useEffect)(() => {
39
+ if (isFormOpen) {
40
+ o_tracking_1.default.view.init({
41
+ selector: '.overlay__content-box',
42
+ getContextData: () => {
43
+ return {
44
+ ...tracking_1.baseTrackingData,
45
+ trigger_action: 'open_question_form',
46
+ };
47
+ },
48
+ });
49
+ }
50
+ }, [isFormOpen]);
33
51
  (0, react_1.useEffect)(() => {
34
52
  if (userDisplayNameError) {
35
53
  dispatch((0, questionSlice_1.updateDisplayNameError)('Sorry, we are unable to update display names. Please try again.'));
@@ -37,7 +55,8 @@ function QuestionForm() {
37
55
  }, [userDisplayNameError]);
38
56
  (0, react_1.useEffect)(() => {
39
57
  setSubmitEnabled(Boolean((profile?.token || (newDisplayName && newDisplayName.length > 0)) &&
40
- question.length >= 20));
58
+ question.length >= MIN_QUESTION_LENGTH &&
59
+ question.length <= MAX_QUESTION_LENGTH));
41
60
  }, [profile?.token, newDisplayName, question]);
42
61
  (0, react_1.useEffect)(() => {
43
62
  // we want to clear the display name error unless the person has attempted
@@ -46,14 +65,6 @@ function QuestionForm() {
46
65
  dispatch((0, questionSlice_1.updateDisplayNameError)(null));
47
66
  }
48
67
  }, [newDisplayName]);
49
- // HANDLING EVENTS
50
- const trackEvent = (trackingData) => {
51
- const event = new CustomEvent('oTracking.event', {
52
- detail: trackingData,
53
- bubbles: true,
54
- });
55
- document.body.dispatchEvent(event);
56
- };
57
68
  const handleHideDisplayName = (e) => {
58
69
  if (!profile?.token && !newDisplayName) {
59
70
  dispatch((0, questionSlice_1.updateDisplayNameError)('You need a display name to ask a question, but you can still post it anonymously.'));
@@ -83,20 +94,25 @@ function QuestionForm() {
83
94
  }
84
95
  if (updatedProfile?.displayName) {
85
96
  // NB: we are using a component act event as analytics team can only support existing categories and actions at this time
86
- trackEvent({
87
- category: 'component',
88
- action: 'act',
97
+ (0, tracking_1.trackEvent)({
98
+ content: {
99
+ uuid: storyId,
100
+ },
89
101
  trigger_action: 'success_set_displayname',
90
102
  });
91
103
  }
92
104
  }
93
105
  try {
106
+ // set nudge to false if the error is a suspect word, as the user can choose to submit it for moderator review
107
+ const errorCode = error?.data?.error?.errorCode ?? undefined;
108
+ const nudge = errorCode !== 'TOXIC_COMMENT';
94
109
  await postQuestion({
95
110
  token: updatedProfile?.token || profile?.token || '',
96
111
  storyId,
97
112
  useStaging,
98
113
  commentsAPIUrl,
99
114
  question: question,
115
+ nudge,
100
116
  postAnonymously,
101
117
  }).unwrap();
102
118
  }
@@ -109,21 +125,6 @@ function QuestionForm() {
109
125
  dispatch((0, questionSlice_1.updateQuestion)(''));
110
126
  }
111
127
  }, [isSuccess]);
112
- if (error) {
113
- // TODO: Remove tracking if confirmed that we can use AWS RUM
114
- // Note: Events related to the Product, not reliability, should still use tracking
115
- trackEvent({
116
- category: 'component',
117
- action: 'view',
118
- trigger_action: 'liveqa_question_form_submit',
119
- custom: [
120
- {
121
- name: 'state',
122
- value: 'error',
123
- },
124
- ],
125
- });
126
- }
127
128
  if (!isFormOpen) {
128
129
  return;
129
130
  }
@@ -132,25 +133,72 @@ function QuestionForm() {
132
133
  // are implemented by Origami team.
133
134
  // Ticket CI-2770 was created to address this in the future.
134
135
  (0, jsx_runtime_1.jsxs)(Overlay_1.default, { title: "Ask a question", closeCallback: () => {
136
+ focusDialogOpener?.();
135
137
  setNewDisplayName(null);
136
138
  dispatch((0, questionSlice_1.closeForm)());
137
- }, 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)([
139
+ reset();
140
+ }, children: [profile.isSubscribed && ((0, jsx_runtime_1.jsx)("div", { className: "overlay__content-box overlay__form-box", "data-testid": "question-form-overlay", "data-trackable-context-trigger_action": "open question form", children: (0, jsx_runtime_1.jsxs)("form", { "aria-busy": isLoading, onSubmit: handleSubmit, "data-trackable": (0, tracking_1.trackingNS)(`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)' } }), (0, jsx_runtime_1.jsxs)("span", { className: "o3-visually-hidden", children: ["Your user name is", ' '] }), profile.displayName] }) })), !profile?.displayName && ((0, jsx_runtime_1.jsxs)("label", { htmlFor: "display-name", className: "question-form__displayname-input o-forms-field", "aria-live": "polite", "aria-atomic": "true", 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)([
138
141
  'question-form__displayname__input',
139
142
  'o-forms-input',
140
143
  'o-forms-input--text',
141
144
  { 'o-forms-input--invalid': !!displayNameError },
142
- ]), 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 ??
143
- '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 }), (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", 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-describedby": error ? 'question-error-message' : undefined }) })] }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
145
+ ]), 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-describedby": "question-form-disclaimer", "aria-invalid": displayNameError ? 'true' : undefined, "aria-errormessage": displayNameError ??
146
+ '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": (0, tracking_1.trackingNS)(`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", className: "o-forms-field question-form__question__input", children: [(0, jsx_runtime_1.jsx)("span", { className: "o-forms-title", children: (0, jsx_runtime_1.jsx)("span", { className: "o3-type-body-highlight", children: "Enter your question" }) }), (0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)([
147
+ 'o-forms-input',
148
+ 'o-forms-input--textarea',
149
+ 'input-textarea__container',
150
+ {
151
+ 'input-textarea__error': question.length > MAX_QUESTION_LENGTH,
152
+ },
153
+ ]), children: [(0, jsx_runtime_1.jsx)("textarea", { id: "question-text", name: "question", className: "input-textarea", value: question, onChange: (e) => dispatch((0, questionSlice_1.updateQuestion)(e.currentTarget.value)), "aria-label": "Your question", "aria-describedby": "question-form-description", placeholder: "Type your question", "aria-invalid": error ? 'true' : undefined, "aria-errormessage": error ? 'question-form-error-message' : undefined }), (0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail input-textarea__counter", "aria-label": `Your question is ${question.length} characters long, maximum allowed is ${MAX_QUESTION_LENGTH}`, children: `${question.length}/${MAX_QUESTION_LENGTH}` })] })] }), (0, jsx_runtime_1.jsx)("span", { id: "question-form-description", "aria-live": "polite", className: (0, classnames_1.default)([
154
+ 'o3-type-detail',
155
+ {
156
+ 'input--textarea__error-message': question.length > MAX_QUESTION_LENGTH,
157
+ },
158
+ ]), children: question.length > MAX_QUESTION_LENGTH
159
+ ? `Questions must not exceed ${MAX_QUESTION_LENGTH} characters.`
160
+ : `Questions must be at least ${MIN_QUESTION_LENGTH} characters long.` }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
144
161
  className: 'overlay__submit-button',
145
162
  type: 'submit',
146
163
  disabled: !submitEnabled,
147
- ariaLabel: question.length < 20
148
- ? 'Submit button (disabled until question is at least 20 characters)'
164
+ ariaLabel: question.length < MIN_QUESTION_LENGTH
165
+ ? `Submit button (disabled until question is at least ${MIN_QUESTION_LENGTH} characters)`
149
166
  : isLoading
150
167
  ? 'Submitting...'
151
168
  : 'Submit your question',
169
+ 'data-trackable': (0, tracking_1.trackingNS)('question-form__submit-button'),
152
170
  },
153
171
  // @ts-expect-error - o3 Button is currently accepting a JSX element as label
154
172
  // but the type definition needs to be updated by Origami team
155
- 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", styles: "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)(MessageBox_1.default, { color: "var(--o3-color-use-case-error)", styles: "o-message--alert o-message--error", title: "Connection issue", text: "Could not submit question: [error messages to come in another ticket]" })), (0, jsx_runtime_1.jsx)(MessageBox_1.default, { styles: "o-message--notice o-message--inform", title: (0, messagesByUserType_1.getTitleByUser)(profile), text: (0, messagesByUserType_1.getTextByUser)(profile) })] }));
173
+ 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", id: "question-form-disclaimer", children: "Submitting a question will create a community profile with FT." }))] }) })), isSuccess && ((0, jsx_runtime_1.jsx)("div", { role: "alert", "aria-live": "polite", children: (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." }) })), (0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", role: "alert", id: "question-form-error-message", children: error && ((0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error, storyId: storyId, errorMapping: {
174
+ // Messages for specific error codes
175
+ REPEAT_POST: {
176
+ title: 'Question already submitted',
177
+ message: "You've already submitted this question. Try asking something different and submit again.",
178
+ },
179
+ REJECTED: {
180
+ title: 'Question rejected',
181
+ message: 'This question has been rejected for language that violates our guidelines.',
182
+ },
183
+ RATE_LIMIT_EXCEEDED: {
184
+ title: 'Too many questions',
185
+ message: 'You are submitting too many questions in a short amount of time. Please wait sometime before submitting another question.',
186
+ },
187
+ TOXIC_COMMENT: {
188
+ title: 'Question blocked',
189
+ message: 'Are you sure? The language in your question might violate our community guidelines. You can edit it, or if you submit, it will need moderator review. This could slow down a reply from our experts',
190
+ },
191
+ serverError: {
192
+ title: 'Server error',
193
+ message: 'There was a server error. Please try submitting your question again.',
194
+ },
195
+ connectionError: {
196
+ title: 'Connection error',
197
+ message: "You can't submit your question. Check your internet connection or browser settings and try again.",
198
+ },
199
+ timeoutError: {
200
+ title: 'Timeout error',
201
+ message: 'The request timed out. Please try submitting your question again.',
202
+ },
203
+ }, operation: "submit-question" })) }), (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) })] }));
156
204
  }
@@ -4,13 +4,15 @@ exports.TERMS_AND_CONDITIONS_URL = exports.COMMUNITY_GUIDELINES_URL = exports.SU
4
4
  exports.getTitleByUser = getTitleByUser;
5
5
  exports.getTextByUser = getTextByUser;
6
6
  const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const qanda_1 = require("../../config/qanda");
8
+ const tracking_1 = require("../../utils/tracking");
7
9
  exports.SIGN_IN_URL = 'https://www.ft.com/login';
8
10
  exports.SUBSCRIBE_URL = 'https://subs.ft.com/products';
9
11
  exports.COMMUNITY_GUIDELINES_URL = 'https://help.ft.com/help/article-queries/commenting-on-an-article/commenting-guidelines';
10
12
  exports.TERMS_AND_CONDITIONS_URL = 'https://help.ft.com/help/legal-privacy/terms-conditions';
11
13
  function getTitleByUser(profile) {
12
14
  if (profile.isSubscribed) {
13
- return 'Q&A guidelines';
15
+ return `${qanda_1.PRODUCT_NAME} guidelines`;
14
16
  }
15
17
  if (profile.isTrialist) {
16
18
  return 'You are still on a trial period';
@@ -20,14 +22,14 @@ function getTitleByUser(profile) {
20
22
  }
21
23
  function getTextByUser(profile) {
22
24
  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" }), "."] }));
25
+ 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": (0, tracking_1.trackingNS)('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": (0, tracking_1.trackingNS)('question-form__terms-and-conditions'), "data-trackable-context-action": "Terms and conditions", href: exports.TERMS_AND_CONDITIONS_URL, target: "_parent", children: "terms" }), "."] }));
24
26
  }
25
27
  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."] }));
28
+ 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": (0, tracking_1.trackingNS)('question-form__subscribe-trialist'), "data-trackable-context-action": "Subscribe", href: exports.SUBSCRIBE_URL, target: "_parent", children: "subscription packages" }), ' ', "to join the conversation."] }));
27
29
  }
28
30
  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."] }));
31
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": (0, tracking_1.trackingNS)('question-form__subscribe-registered'), "data-trackable-context-action": "Subscribe", href: exports.SUBSCRIBE_URL, target: "_parent", children: "Subscribe" }), ' ', "to join the conversation."] }));
30
32
  }
31
33
  // 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."] }));
34
+ 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": (0, tracking_1.trackingNS)('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": (0, tracking_1.trackingNS)('question-form__subscribe-anonymous'), "data-trackable-context-action": "Subscribe", href: exports.SUBSCRIBE_URL, target: "_parent", children: "subscribe" }), ' ', "to join the conversation."] }));
33
35
  }
@@ -6,25 +6,52 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const jsx_runtime_1 = require("preact/jsx-runtime");
7
7
  const react_1 = require("react");
8
8
  const react_redux_1 = require("react-redux");
9
+ const o_tracking_1 = __importDefault(require("@financial-times/o-tracking"));
9
10
  const index_1 = __importDefault(require("../QandaBlock/index"));
10
11
  const QandaProvider_1 = require("../QandaProvider");
11
12
  const Loader_1 = __importDefault(require("../Loader"));
12
13
  const comments_api_1 = require("../../services/comments-api");
13
- function Stream() {
14
+ const ErrorMessage_1 = __importDefault(require("../ErrorMessage"));
15
+ const tracking_1 = require("../../utils/tracking");
16
+ function Stream({ status = 'pre-live' }) {
14
17
  const latestAnsweredQuestionId = (0, react_redux_1.useSelector)((state) => state.stream.latestAnsweredQuestionId);
15
18
  const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
16
19
  const { data, error, isLoading } = (0, comments_api_1.useGetQandAStreamQuery)({
17
20
  storyId,
18
21
  useStaging,
19
22
  commentsAPIUrl,
20
- });
23
+ status,
24
+ },
25
+ // In comments api, the stream endpoint uses different queries based on the Q&A status (live stream vs pre-live stream),
26
+ // If the status changes and Qanda receives an update,
27
+ // we need to force RTK to re-fetch the data and show the appropriate stream.
28
+ { refetchOnMountOrArgChange: true });
21
29
  (0, react_1.useEffect)(() => {
22
30
  const element = document.getElementById(latestAnsweredQuestionId);
23
31
  element?.scrollIntoView({ behavior: 'smooth' });
24
32
  }, [latestAnsweredQuestionId]);
25
- 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'
26
- ? 'Top questions so far'
27
- : '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)("p", { children: "Failed to load Q&A stream" }), data &&
33
+ (0, react_1.useEffect)(() => {
34
+ if (!data || !status)
35
+ return;
36
+ o_tracking_1.default.view.init({
37
+ intersectionObserverThreshold: 0.0001,
38
+ selector: '.qanda__stream',
39
+ getContextData: () => {
40
+ return {
41
+ ...tracking_1.baseTrackingData,
42
+ custom: [
43
+ {
44
+ name: 'status',
45
+ value: status,
46
+ },
47
+ ],
48
+ };
49
+ },
50
+ });
51
+ }, [data, status]);
52
+ return ((0, jsx_runtime_1.jsxs)("section", { className: "qanda__stream", "data-trackable-context-trigger_action": "view qanda container", children: [data && data.qandas.length > 0 && ((0, jsx_runtime_1.jsx)("h2", { className: "o3-type-title-md qanda__stream-title", id: "qanda__stream-title", children: data.status && data.status === 'pre-live'
53
+ ? 'Need inspiration? A selection of reader questions so far'
54
+ : '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 &&
28
55
  data.qandas &&
29
56
  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)))] })] }));
30
57
  }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("preact/jsx-runtime");
4
+ const tracking_1 = require("../../utils/tracking");
5
+ function Survey({ storyId, status }) {
6
+ 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}`, className: "o-message__actions__primary", target: "_blank", rel: "noopener noreferrer", "data-trackable": (0, tracking_1.trackingNS)('survey_link'), children: "Share your feedback" }) })] }) }) }));
7
+ }
8
+ exports.default = Survey;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PRODUCT_NAME = void 0;
4
+ // This file contains configuration settings for the Q&A (AKA Ask an Expert) features of the application.
5
+ // User facing product name
6
+ exports.PRODUCT_NAME = 'Ask an Expert';