@financial-times/qanda-ui 0.0.1-beta.19 → 0.0.1-beta.20

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 (35) hide show
  1. package/dist/cjs/components/ErrorMessage/humanReadableError.js +74 -0
  2. package/dist/cjs/components/ErrorMessage/index.js +63 -0
  3. package/dist/cjs/components/MessageBox/index.js +7 -0
  4. package/dist/cjs/components/QandaContainer.js +24 -5
  5. package/dist/cjs/components/QuestionForm/index.js +36 -27
  6. package/dist/cjs/components/Stream/index.js +2 -1
  7. package/dist/cjs/components/Survey/index.js +36 -0
  8. package/dist/cjs/services/comments-api.js +1 -1
  9. package/dist/cjs/utils/strings.js +16 -0
  10. package/dist/cjs/utils/tracking.js +11 -0
  11. package/dist/esm/components/ErrorMessage/humanReadableError.js +71 -0
  12. package/dist/esm/components/ErrorMessage/index.js +57 -0
  13. package/dist/esm/components/MessageBox/index.js +5 -0
  14. package/dist/esm/components/QandaContainer.js +25 -6
  15. package/dist/esm/components/QuestionForm/index.js +35 -26
  16. package/dist/esm/components/Stream/index.js +2 -1
  17. package/dist/esm/components/Survey/index.js +34 -0
  18. package/dist/esm/services/comments-api.js +2 -2
  19. package/dist/esm/utils/strings.js +13 -0
  20. package/dist/esm/utils/tracking.js +7 -0
  21. package/dist/qanda.scss +41 -17
  22. package/dist/types/components/ErrorMessage/humanReadableError.d.ts +29 -0
  23. package/dist/types/components/ErrorMessage/index.d.ts +8 -0
  24. package/dist/types/components/{Overlay/MessageBox.d.ts → MessageBox/index.d.ts} +3 -2
  25. package/dist/types/components/Survey/index.d.ts +6 -0
  26. package/dist/types/services/comments-api.d.ts +6 -2
  27. package/dist/types/store/index.d.ts +12 -4
  28. package/dist/types/utils/strings.d.ts +1 -0
  29. package/dist/types/utils/tracking.d.ts +2 -0
  30. package/package.json +3 -3
  31. package/dist/cjs/components/Overlay/MessageBox.js +0 -7
  32. package/dist/cjs/components/UserInfo.js +0 -22
  33. package/dist/esm/components/Overlay/MessageBox.js +0 -5
  34. package/dist/esm/components/UserInfo.js +0 -20
  35. package/dist/types/components/UserInfo.d.ts +0 -2
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const baseErrorMessages = {
4
+ generic: {
5
+ title: 'An error occurred',
6
+ message: 'Please try again later.',
7
+ },
8
+ serverError: {
9
+ title: 'Server error',
10
+ message: 'There was a server error. Please try again later.',
11
+ },
12
+ connectionError: {
13
+ title: 'Connection error',
14
+ message: 'Check your internet connection or browser settings and try again.',
15
+ },
16
+ timeoutError: {
17
+ title: 'Timeout error',
18
+ message: 'The request timed out. You may be on a slow connection. Please try again later.',
19
+ },
20
+ };
21
+ class HumanReadableError {
22
+ constructor(error, customErrorMessages = {}) {
23
+ // Default to generic error message
24
+ this._humanReadableError = baseErrorMessages.generic;
25
+ this._unMappedError = null;
26
+ const errorMessages = {
27
+ ...baseErrorMessages,
28
+ ...customErrorMessages,
29
+ };
30
+ try {
31
+ if ('status' in error) {
32
+ if (error.status === 'FETCH_ERROR') {
33
+ this._humanReadableError = errorMessages.connectionError;
34
+ }
35
+ else if (typeof error.status === 'number' && error.status >= 500) {
36
+ this._humanReadableError = errorMessages.serverError;
37
+ }
38
+ else if (typeof error.status === 'number' && error.status === 400) {
39
+ // Assuming as an error code otherwise will throw an error and we will fallback to generic error
40
+ const typedError = error;
41
+ this._humanReadableError =
42
+ errorMessages[typedError.data.error.errorCode];
43
+ }
44
+ }
45
+ else if ('name' in error) {
46
+ if (error.name === 'AbortError') {
47
+ this._humanReadableError = errorMessages.timeoutError;
48
+ }
49
+ }
50
+ }
51
+ catch (err) {
52
+ // Fallback to generic error message if we cannot parse the error correctly
53
+ this._humanReadableError = errorMessages.generic;
54
+ this._unMappedError = {
55
+ name: err && typeof err === 'object' && 'name' in err
56
+ ? String(err.name)
57
+ : '',
58
+ message: err && typeof err === 'object' && 'message' in err
59
+ ? String(err.message)
60
+ : '',
61
+ };
62
+ }
63
+ }
64
+ get title() {
65
+ return this._humanReadableError.title;
66
+ }
67
+ get message() {
68
+ return this._humanReadableError.message;
69
+ }
70
+ get unmappedError() {
71
+ return this._unMappedError;
72
+ }
73
+ }
74
+ exports.default = HumanReadableError;
@@ -0,0 +1,63 @@
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 = ErrorMessage;
7
+ const jsx_runtime_1 = require("preact/jsx-runtime");
8
+ const MessageBox_1 = __importDefault(require("../MessageBox"));
9
+ const tracking_1 = require("../../utils/tracking");
10
+ const humanReadableError_1 = __importDefault(require("./humanReadableError"));
11
+ const react_1 = require("react");
12
+ const strings_1 = require("../../utils/strings");
13
+ function ErrorMessage({ error, storyId, errorMapping = {}, operation = 'not-specified', id = '', }) {
14
+ const humanReadableError = new humanReadableError_1.default(error, errorMapping);
15
+ let errorDetails = [];
16
+ // If the error is unmapped, we log it for debugging purposes
17
+ if (humanReadableError.unmappedError) {
18
+ errorDetails = [
19
+ {
20
+ name: 'error_name',
21
+ value: humanReadableError.unmappedError.name,
22
+ },
23
+ {
24
+ name: 'error_message',
25
+ value: humanReadableError.unmappedError.message,
26
+ },
27
+ ];
28
+ }
29
+ else {
30
+ errorDetails = [
31
+ {
32
+ name: 'error',
33
+ value: (0, strings_1.cleanString)(humanReadableError.title),
34
+ },
35
+ {
36
+ name: 'error_message',
37
+ value: (0, strings_1.cleanString)(humanReadableError.message),
38
+ },
39
+ ];
40
+ }
41
+ (0, react_1.useEffect)(() => {
42
+ // Critical operation are tracked by the reliability service using AWS RUM
43
+ // We still log this to monitor untracked messages to the users.
44
+ (0, tracking_1.trackEvent)({
45
+ category: 'component',
46
+ action: 'act',
47
+ content: {
48
+ asset_type: 'article',
49
+ uuid: storyId,
50
+ },
51
+ trigger_action: operation,
52
+ custom: [
53
+ {
54
+ name: 'state',
55
+ value: 'error',
56
+ },
57
+ ...errorDetails,
58
+ ],
59
+ });
60
+ // Ensure trackEvent is only called once
61
+ }, []);
62
+ return ((0, jsx_runtime_1.jsx)(MessageBox_1.default, { color: "var(--o3-color-use-case-error)", extraClassNames: "o-message--alert o-message--error", title: humanReadableError.title, text: humanReadableError.message, id: id }));
63
+ }
@@ -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;
@@ -9,7 +9,7 @@ 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"));
@@ -22,12 +22,25 @@ const comments_api_1 = require("../services/comments-api");
22
22
  function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
23
23
  const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
24
24
  const dispatch = (0, react_redux_1.useDispatch)();
25
+ const [liveMessage, setLiveMessage] = (0, react_1.useState)('');
25
26
  const { data, error, isLoading } = (0, comments_api_1.useGetQandAStreamQuery)({
26
27
  storyId,
27
28
  useStaging,
28
29
  commentsAPIUrl,
29
30
  status: 'pre-live',
30
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]);
31
44
  const updatedQA = (0, comments_api_1.useUpdatedQA)({
32
45
  storyId,
33
46
  useStaging,
@@ -62,9 +75,15 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
62
75
  selector: '.qanda-container',
63
76
  getContextData: () => {
64
77
  return {
65
- componentContentId: storyId,
66
- component: 'liveqa',
67
- type: 'article',
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
+ },
68
87
  custom: [
69
88
  {
70
89
  name: 'status',
@@ -75,6 +94,6 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
75
94
  },
76
95
  });
77
96
  }, [data, status]);
78
- 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 }), status !== 'close' && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(UserInfo_1.default, {}) })), (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 }))] }))] }));
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 })] }));
79
98
  }
80
99
  exports.default = QandaContainer;
@@ -14,10 +14,12 @@ const comments_api_1 = require("../../services/comments-api");
14
14
  const Overlay_1 = __importDefault(require("../Overlay"));
15
15
  const Loader_1 = __importDefault(require("../Loader"));
16
16
  const o3_button_1 = require("@financial-times/o3-button");
17
- const MessageBox_1 = __importDefault(require("../Overlay/MessageBox"));
17
+ const MessageBox_1 = __importDefault(require("../MessageBox"));
18
+ const ErrorMessage_1 = __importDefault(require("../ErrorMessage"));
18
19
  const user_1 = require("../../store/user");
19
20
  const validate_display_name_1 = __importDefault(require("../../utils/validate-display-name"));
20
21
  const messagesByUserType_1 = require("./messagesByUserType");
22
+ const tracking_1 = require("../../utils/tracking");
21
23
  function QuestionForm() {
22
24
  const dispatch = (0, react_redux_1.useDispatch)();
23
25
  const { storyId, useStaging, commentsAPIUrl, maxQuestionLength } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
@@ -46,14 +48,6 @@ function QuestionForm() {
46
48
  dispatch((0, questionSlice_1.updateDisplayNameError)(null));
47
49
  }
48
50
  }, [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
51
  const handleHideDisplayName = (e) => {
58
52
  if (!profile?.token && !newDisplayName) {
59
53
  dispatch((0, questionSlice_1.updateDisplayNameError)('You need a display name to ask a question, but you can still post it anonymously.'));
@@ -83,7 +77,7 @@ function QuestionForm() {
83
77
  }
84
78
  if (updatedProfile?.displayName) {
85
79
  // NB: we are using a component act event as analytics team can only support existing categories and actions at this time
86
- trackEvent({
80
+ (0, tracking_1.trackEvent)({
87
81
  category: 'component',
88
82
  action: 'act',
89
83
  trigger_action: 'success_set_displayname',
@@ -109,21 +103,6 @@ function QuestionForm() {
109
103
  dispatch((0, questionSlice_1.updateQuestion)(''));
110
104
  }
111
105
  }, [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
106
  if (!isFormOpen) {
128
107
  return;
129
108
  }
@@ -140,7 +119,7 @@ function QuestionForm() {
140
119
  'o-forms-input--text',
141
120
  { 'o-forms-input--invalid': !!displayNameError },
142
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 ??
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, "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", 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: {
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: {
144
123
  className: 'overlay__submit-button',
145
124
  type: 'submit',
146
125
  disabled: !submitEnabled,
@@ -152,5 +131,35 @@ function QuestionForm() {
152
131
  },
153
132
  // @ts-expect-error - o3 Button is currently accepting a JSX element as label
154
133
  // 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) })] }));
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) })] }));
156
165
  }
@@ -10,6 +10,7 @@ const index_1 = __importDefault(require("../QandaBlock/index"));
10
10
  const QandaProvider_1 = require("../QandaProvider");
11
11
  const Loader_1 = __importDefault(require("../Loader"));
12
12
  const comments_api_1 = require("../../services/comments-api");
13
+ const ErrorMessage_1 = __importDefault(require("../ErrorMessage"));
13
14
  function Stream({ status = 'pre-live' }) {
14
15
  const latestAnsweredQuestionId = (0, react_redux_1.useSelector)((state) => state.stream.latestAnsweredQuestionId);
15
16
  const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
@@ -29,7 +30,7 @@ function Stream({ status = 'pre-live' }) {
29
30
  }, [latestAnsweredQuestionId]);
30
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'
31
32
  ? 'Top questions so far'
32
- : '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
+ : '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 &&
33
34
  data.qandas &&
34
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)))] })] }));
35
36
  }
@@ -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;
@@ -121,7 +121,7 @@ exports.nextCommentsApi = (0, react_1.createApi)({
121
121
  },
122
122
  body: JSON.stringify({ question, postAnonymously, token }),
123
123
  }),
124
- transformResponse: (response, meta, arg) => {
124
+ transformResponse: (response) => {
125
125
  return response.data;
126
126
  },
127
127
  }),
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cleanString = cleanString;
4
+ /*
5
+ * Remove special characters and replace spaces with underscores
6
+ * @param {string} str - The input string to be cleaned
7
+ * @returns {string} - The cleaned string with special characters removed and spaces replaced with underscores
8
+ */
9
+ function cleanString(str) {
10
+ return str
11
+ .normalize('NFD') // Decompose accented characters
12
+ .replace(/[\u0300-\u036f]/g, '') // Remove diacritics
13
+ .replace(/[^a-zA-Z0-9 ]/g, '') // Remove non-ASCII letters, numbers, and spaces
14
+ .trim() // Trim leading/trailing spaces
15
+ .replace(/\s+/g, '_'); // Replace spaces with underscores
16
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.trackEvent = void 0;
4
+ const trackEvent = (trackingData) => {
5
+ const event = new CustomEvent('oTracking.event', {
6
+ detail: trackingData,
7
+ bubbles: true,
8
+ });
9
+ document.body.dispatchEvent(event);
10
+ };
11
+ exports.trackEvent = trackEvent;
@@ -0,0 +1,71 @@
1
+ const baseErrorMessages = {
2
+ generic: {
3
+ title: 'An error occurred',
4
+ message: 'Please try again later.',
5
+ },
6
+ serverError: {
7
+ title: 'Server error',
8
+ message: 'There was a server error. Please try again later.',
9
+ },
10
+ connectionError: {
11
+ title: 'Connection error',
12
+ message: 'Check your internet connection or browser settings and try again.',
13
+ },
14
+ timeoutError: {
15
+ title: 'Timeout error',
16
+ message: 'The request timed out. You may be on a slow connection. Please try again later.',
17
+ },
18
+ };
19
+ export default class HumanReadableError {
20
+ constructor(error, customErrorMessages = {}) {
21
+ // Default to generic error message
22
+ this._humanReadableError = baseErrorMessages.generic;
23
+ this._unMappedError = null;
24
+ const errorMessages = {
25
+ ...baseErrorMessages,
26
+ ...customErrorMessages,
27
+ };
28
+ try {
29
+ if ('status' in error) {
30
+ if (error.status === 'FETCH_ERROR') {
31
+ this._humanReadableError = errorMessages.connectionError;
32
+ }
33
+ else if (typeof error.status === 'number' && error.status >= 500) {
34
+ this._humanReadableError = errorMessages.serverError;
35
+ }
36
+ else if (typeof error.status === 'number' && error.status === 400) {
37
+ // Assuming as an error code otherwise will throw an error and we will fallback to generic error
38
+ const typedError = error;
39
+ this._humanReadableError =
40
+ errorMessages[typedError.data.error.errorCode];
41
+ }
42
+ }
43
+ else if ('name' in error) {
44
+ if (error.name === 'AbortError') {
45
+ this._humanReadableError = errorMessages.timeoutError;
46
+ }
47
+ }
48
+ }
49
+ catch (err) {
50
+ // Fallback to generic error message if we cannot parse the error correctly
51
+ this._humanReadableError = errorMessages.generic;
52
+ this._unMappedError = {
53
+ name: err && typeof err === 'object' && 'name' in err
54
+ ? String(err.name)
55
+ : '',
56
+ message: err && typeof err === 'object' && 'message' in err
57
+ ? String(err.message)
58
+ : '',
59
+ };
60
+ }
61
+ }
62
+ get title() {
63
+ return this._humanReadableError.title;
64
+ }
65
+ get message() {
66
+ return this._humanReadableError.message;
67
+ }
68
+ get unmappedError() {
69
+ return this._unMappedError;
70
+ }
71
+ }
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ import MessageBox from '../MessageBox';
3
+ import { trackEvent } from '../../utils/tracking';
4
+ import HumanReadableError from './humanReadableError';
5
+ import { useEffect } from 'react';
6
+ import { cleanString } from '../../utils/strings';
7
+ export default function ErrorMessage({ error, storyId, errorMapping = {}, operation = 'not-specified', id = '', }) {
8
+ const humanReadableError = new HumanReadableError(error, errorMapping);
9
+ let errorDetails = [];
10
+ // If the error is unmapped, we log it for debugging purposes
11
+ if (humanReadableError.unmappedError) {
12
+ errorDetails = [
13
+ {
14
+ name: 'error_name',
15
+ value: humanReadableError.unmappedError.name,
16
+ },
17
+ {
18
+ name: 'error_message',
19
+ value: humanReadableError.unmappedError.message,
20
+ },
21
+ ];
22
+ }
23
+ else {
24
+ errorDetails = [
25
+ {
26
+ name: 'error',
27
+ value: cleanString(humanReadableError.title),
28
+ },
29
+ {
30
+ name: 'error_message',
31
+ value: cleanString(humanReadableError.message),
32
+ },
33
+ ];
34
+ }
35
+ useEffect(() => {
36
+ // Critical operation are tracked by the reliability service using AWS RUM
37
+ // We still log this to monitor untracked messages to the users.
38
+ trackEvent({
39
+ category: 'component',
40
+ action: 'act',
41
+ content: {
42
+ asset_type: 'article',
43
+ uuid: storyId,
44
+ },
45
+ trigger_action: operation,
46
+ custom: [
47
+ {
48
+ name: 'state',
49
+ value: 'error',
50
+ },
51
+ ...errorDetails,
52
+ ],
53
+ });
54
+ // Ensure trackEvent is only called once
55
+ }, []);
56
+ return (_jsx(MessageBox, { color: "var(--o3-color-use-case-error)", extraClassNames: "o-message--alert o-message--error", title: humanReadableError.title, text: humanReadableError.message, id: id }));
57
+ }
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ function MessageBox({ color, extraClassNames = '', text, title, id = '', }) {
3
+ return (_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: _jsx("div", { class: "o-message__container", children: _jsxs("div", { class: "message-box__content", children: [_jsx("span", { style: { color }, class: "o3-type-body-highlight", children: title }), _jsx("span", { style: { color }, class: "o3-type-body-base", children: text })] }) }) }));
4
+ }
5
+ export default MessageBox;
@@ -1,10 +1,10 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "preact/jsx-runtime";
2
- import { useEffect, useContext } from 'react';
2
+ import { useState, useEffect, useContext } from 'react';
3
3
  import { useDispatch, useSelector } from 'react-redux';
4
4
  import { getUserProfile } from '../store/user';
5
5
  import { setExpertView } from '../store/expertSlice';
6
6
  import QuestionForm from './QuestionForm';
7
- import UserInfo from './UserInfo';
7
+ import Survey from './Survey';
8
8
  import ExpertDrawer from './ExpertDrawer';
9
9
  import Stream from './Stream';
10
10
  import Calendar from './Calendar';
@@ -17,12 +17,25 @@ import { useGetQandAStreamQuery, useUpdatedQA, useGetUserRolesQuery, nextComment
17
17
  function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
18
18
  const { storyId, useStaging, commentsAPIUrl } = useContext(QandaContext);
19
19
  const dispatch = useDispatch();
20
+ const [liveMessage, setLiveMessage] = useState('');
20
21
  const { data, error, isLoading } = useGetQandAStreamQuery({
21
22
  storyId,
22
23
  useStaging,
23
24
  commentsAPIUrl,
24
25
  status: 'pre-live',
25
26
  });
27
+ const [ariaMessage, setAriaMessage] = useState('');
28
+ useEffect(() => {
29
+ if (isLoading) {
30
+ setAriaMessage('Loading Q&A');
31
+ }
32
+ else if (error) {
33
+ setAriaMessage('Error loading Q&A');
34
+ }
35
+ else if (data) {
36
+ setAriaMessage('Q&A stream has been updated');
37
+ }
38
+ }, [isLoading, error, data]);
26
39
  const updatedQA = useUpdatedQA({
27
40
  storyId,
28
41
  useStaging,
@@ -57,9 +70,15 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
57
70
  selector: '.qanda-container',
58
71
  getContextData: () => {
59
72
  return {
60
- componentContentId: storyId,
61
- component: 'liveqa',
62
- type: 'article',
73
+ component: {
74
+ name: 'liveqa',
75
+ type: 'container',
76
+ id: '663ab2ca-77ca-4b50-873d-1fd2ff21ff08',
77
+ },
78
+ content: {
79
+ asset_type: 'article',
80
+ uuid: storyId,
81
+ },
63
82
  custom: [
64
83
  {
65
84
  name: 'status',
@@ -70,6 +89,6 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
70
89
  },
71
90
  });
72
91
  }, [data, status]);
73
- return (_jsxs("div", { className: "qanda-container", "data-trackable": "liveqa", children: [error && (_jsxs("p", { children: ["We are not able to load the Q&A at the moment. Please try again later. Error:", error] })), !isLoading && !error && (_jsxs(_Fragment, { children: [status !== 'close' && (_jsxs(_Fragment, { children: [showExpertView ? _jsx(ExpertDrawer, {}) : _jsx(QuestionForm, {}), _jsx(CountdownTimer, { container: countdownTimerContainer })] })), status === 'pre-live' && _jsx(Calendar, {}), _jsx(Stream, { status: status }), status !== 'close' && (_jsx(_Fragment, { children: _jsx(UserInfo, {}) })), (status === 'live' || (status === 'pre-live' && showExpertView)) && (_jsx(FloatingActionBar, { showLiveIndicator: status === 'live' })), status !== 'close' && !showExpertView && (_jsx(EmbedFormField, { container: embedFormFieldContainer }))] }))] }));
92
+ return (_jsxs("div", { className: "qanda-container", "data-trackable": "liveqa", children: [error && (_jsxs("p", { children: ["We are not able to load the Q&A at the moment. Please try again later. Error:", error] })), !isLoading && !error && (_jsxs(_Fragment, { children: [status !== 'close' && (_jsxs(_Fragment, { children: [showExpertView ? _jsx(ExpertDrawer, {}) : _jsx(QuestionForm, {}), _jsx(CountdownTimer, { container: countdownTimerContainer })] })), status === 'pre-live' && _jsx(Calendar, {}), _jsx(Stream, { status: status }), _jsx(Survey, { storyId: storyId, status: status }), (status === 'live' || (status === 'pre-live' && showExpertView)) && (_jsx(FloatingActionBar, { showLiveIndicator: status === 'live' })), status !== 'close' && !showExpertView && (_jsx(EmbedFormField, { container: embedFormFieldContainer }))] })), _jsx("div", { "aria-live": "polite", "aria-atomic": "true", className: "o3-visually-hidden", children: ariaMessage })] }));
74
93
  }
75
94
  export default QandaContainer;
@@ -8,10 +8,12 @@ import { usePostQuestionMutation } from '../../services/comments-api';
8
8
  import Overlay from '../Overlay';
9
9
  import Loader from '../Loader';
10
10
  import { Button } from '@financial-times/o3-button';
11
- import MessageBox from '../Overlay/MessageBox';
11
+ import MessageBox from '../MessageBox';
12
+ import ErrorMessage from '../ErrorMessage';
12
13
  import { getUserProfile } from '../../store/user';
13
14
  import validateDisplayName from '../../utils/validate-display-name';
14
15
  import { getTitleByUser, getTextByUser } from './messagesByUserType';
16
+ import { trackEvent } from '../../utils/tracking';
15
17
  export default function QuestionForm() {
16
18
  const dispatch = useDispatch();
17
19
  const { storyId, useStaging, commentsAPIUrl, maxQuestionLength } = useContext(QandaContext);
@@ -40,14 +42,6 @@ export default function QuestionForm() {
40
42
  dispatch(updateDisplayNameError(null));
41
43
  }
42
44
  }, [newDisplayName]);
43
- // HANDLING EVENTS
44
- const trackEvent = (trackingData) => {
45
- const event = new CustomEvent('oTracking.event', {
46
- detail: trackingData,
47
- bubbles: true,
48
- });
49
- document.body.dispatchEvent(event);
50
- };
51
45
  const handleHideDisplayName = (e) => {
52
46
  if (!profile?.token && !newDisplayName) {
53
47
  dispatch(updateDisplayNameError('You need a display name to ask a question, but you can still post it anonymously.'));
@@ -103,21 +97,6 @@ export default function QuestionForm() {
103
97
  dispatch(updateQuestion(''));
104
98
  }
105
99
  }, [isSuccess]);
106
- if (error) {
107
- // TODO: Remove tracking if confirmed that we can use AWS RUM
108
- // Note: Events related to the Product, not reliability, should still use tracking
109
- trackEvent({
110
- category: 'component',
111
- action: 'view',
112
- trigger_action: 'liveqa_question_form_submit',
113
- custom: [
114
- {
115
- name: 'state',
116
- value: 'error',
117
- },
118
- ],
119
- });
120
- }
121
100
  if (!isFormOpen) {
122
101
  return;
123
102
  }
@@ -134,7 +113,7 @@ export default function QuestionForm() {
134
113
  'o-forms-input--text',
135
114
  { 'o-forms-input--invalid': !!displayNameError },
136
115
  ]), children: [_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 ??
137
- 'question_form__displayname-error-message' }), displayNameError && (_jsx("span", { className: "question-form__displayname__input__error o3-type-detail", id: "question_form__displayname-error-message", children: _jsx("span", { children: displayNameError }) }))] })] }))] }), _jsx("div", { className: "o-forms-field", children: _jsxs("label", { className: "o-forms-input o-forms-input--checkbox", htmlFor: "post-anonymously", children: [_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'}` }), _jsx("span", { className: "o-forms-input__label", children: "Post anonymously (optional)" })] }) }), _jsxs("label", { for: "question-text", class: "o-forms-field", children: [_jsx("span", { class: "o-forms-title", children: _jsx("span", { class: "o3-type-body-highlight", children: "Enter your question" }) }), _jsx("span", { class: "o-forms-input o-forms-input--textarea", children: _jsx("textarea", { id: "question-text", name: "question", value: question, onChange: (e) => dispatch(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 }) })] }), _jsx(Button, { attributes: {
116
+ 'question_form__displayname-error-message' }), displayNameError && (_jsx("span", { className: "question-form__displayname__input__error o3-type-detail", id: "question_form__displayname-error-message", children: _jsx("span", { children: displayNameError }) }))] })] }))] }), _jsx("div", { className: "o-forms-field", children: _jsxs("label", { className: "o-forms-input o-forms-input--checkbox", htmlFor: "post-anonymously", children: [_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'}` }), _jsx("span", { className: "o-forms-input__label", children: "Post anonymously (optional)" })] }) }), _jsxs("label", { for: "question-text", class: "o-forms-field question-form__question__input", children: [_jsx("span", { class: "o-forms-title", children: _jsx("span", { class: "o3-type-body-highlight", children: "Enter your question" }) }), _jsx("span", { class: "o-forms-input o-forms-input--textarea", children: _jsx("textarea", { id: "question-text", name: "question", value: question, onChange: (e) => dispatch(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" }) })] }), _jsx("span", { className: "o3-type-detail", children: "Questions must be at least 20 characters long." }), _jsx(Button, { attributes: {
138
117
  className: 'overlay__submit-button',
139
118
  type: 'submit',
140
119
  disabled: !submitEnabled,
@@ -146,5 +125,35 @@ export default function QuestionForm() {
146
125
  },
147
126
  // @ts-expect-error - o3 Button is currently accepting a JSX element as label
148
127
  // but the type definition needs to be updated by Origami team
149
- label: isLoading ? _jsx(Loader, {}) : 'Submit your question', type: "primary", theme: "mono" }), !profile?.token && (_jsx("span", { className: "o3-type-detail", children: "Submitting a question will create a community profile with FT." }))] }) })), isSuccess && (_jsx(MessageBox, { 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 && (_jsx(MessageBox, { 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]" })), _jsx(MessageBox, { styles: "o-message--notice o-message--inform", title: getTitleByUser(profile), text: getTextByUser(profile) })] }));
128
+ label: isLoading ? _jsx(Loader, {}) : 'Submit your question', type: "primary", theme: "mono" }), !profile?.token && (_jsx("span", { className: "o3-type-detail", children: "Submitting a question will create a community profile with FT." }))] }) })), isSuccess && (_jsx(MessageBox, { 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 && (_jsx(ErrorMessage, { error: error, storyId: storyId, errorMapping: {
129
+ // Messages for specific error codes
130
+ REPEAT_POST: {
131
+ title: 'Question already submitted',
132
+ message: "You've already submitted this question. Try asking something different and submit again.",
133
+ },
134
+ REJECTED: {
135
+ title: 'Question rejected',
136
+ message: 'This question has been rejected for language that violates our guidelines.',
137
+ },
138
+ RATE_LIMIT_EXCEEDED: {
139
+ title: 'Too many questions',
140
+ message: 'You are submitting too many questions in a short amount of time. Please wait sometime before submitting another question.',
141
+ },
142
+ TOXIC_COMMENT: {
143
+ title: 'Question blocked',
144
+ message: 'Are you sure? The language in your question might violate our community guidelines. You can edit it or submit it for moderator review.',
145
+ },
146
+ serverError: {
147
+ title: 'Server error',
148
+ message: 'There was a server error. Please try submitting your question again.',
149
+ },
150
+ connectionError: {
151
+ title: 'Connection error',
152
+ message: "You can't submit your question. Check your internet connection or browser settings and try again.",
153
+ },
154
+ timeoutError: {
155
+ title: 'Timeout error',
156
+ message: 'The request timed out. Please try submitting your question again.',
157
+ },
158
+ }, operation: "submit-question", id: "question-form-error-message" })), _jsx(MessageBox, { extraClassNames: "o-message--notice o-message--inform", title: getTitleByUser(profile), text: getTextByUser(profile) })] }));
150
159
  }
@@ -5,6 +5,7 @@ import QandaBlock from '../QandaBlock/index';
5
5
  import { QandaContext } from '../QandaProvider';
6
6
  import Loader from '../Loader';
7
7
  import { useGetQandAStreamQuery } from '../../services/comments-api';
8
+ import ErrorMessage from '../ErrorMessage';
8
9
  function Stream({ status = 'pre-live' }) {
9
10
  const latestAnsweredQuestionId = useSelector((state) => state.stream.latestAnsweredQuestionId);
10
11
  const { storyId, useStaging, commentsAPIUrl } = useContext(QandaContext);
@@ -24,7 +25,7 @@ function Stream({ status = 'pre-live' }) {
24
25
  }, [latestAnsweredQuestionId]);
25
26
  return (_jsxs("section", { className: "qanda__stream", children: [_jsx("h2", { className: "o3-type-title-md qanda__stream-title", children: data && data.status && data.status === 'pre-live'
26
27
  ? 'Top questions so far'
27
- : 'What our readers are asking' }), _jsxs("div", { className: "qanda__blocks-container", "data-testid": "qanda__blocks-container", id: "qanda__blocks-container", "aria-busy": isLoading, children: [isLoading && (_jsx(Loader, { theme: "dark", extraClassNames: "qanda__stream-loader" })), error && _jsx("p", { children: "Failed to load Q&A stream" }), data &&
28
+ : 'What our readers are asking' }), _jsxs("div", { className: "qanda__blocks-container", "data-testid": "qanda__blocks-container", id: "qanda__blocks-container", "aria-busy": isLoading, children: [isLoading && (_jsx(Loader, { theme: "dark", extraClassNames: "qanda__stream-loader" })), error && (_jsx(ErrorMessage, { error: error, storyId: storyId, operation: "loading-stream" })), data &&
28
29
  data.qandas &&
29
30
  data.qandas.map((qandaBlock) => (_jsx(QandaBlock, { 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
31
  }
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ function Survey({ storyId, status }) {
3
+ const trackEvent = (trackingData) => {
4
+ const event = new CustomEvent('oTracking.event', {
5
+ detail: trackingData,
6
+ bubbles: true,
7
+ });
8
+ document.body.dispatchEvent(event);
9
+ };
10
+ const handleClick = () => {
11
+ trackEvent({
12
+ category: 'component',
13
+ component: {
14
+ name: 'liveqa',
15
+ type: 'container',
16
+ id: '663ab2ca-77ca-4b50-873d-1fd2ff21ff08',
17
+ },
18
+ action: 'act',
19
+ trigger_action: 'liveqa_survey_open',
20
+ content: {
21
+ asset_type: 'article',
22
+ uuid: storyId,
23
+ },
24
+ custom: [
25
+ {
26
+ name: 'status',
27
+ value: status,
28
+ },
29
+ ],
30
+ });
31
+ };
32
+ return (_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: _jsx("div", { className: "o-message__container", children: _jsxs("div", { className: "o-message__content", children: [_jsx("p", { className: "o-message__content-main", style: { fontSize: 'var(--o3-type-body-base-font-size)' }, children: "How was your experience?" }), _jsx("div", { className: "o-message__actions", children: _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" }) })] }) }) }));
33
+ }
34
+ export default Survey;
@@ -1,4 +1,4 @@
1
- import { createApi, fetchBaseQuery, } from '@reduxjs/toolkit/query/react';
1
+ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
2
2
  // TODO: ensure that users don't miss out on updates in this window
3
3
  // https://financialtimes.atlassian.net/browse/CI-2715
4
4
  const retryDelay = 1000;
@@ -118,7 +118,7 @@ export const nextCommentsApi = createApi({
118
118
  },
119
119
  body: JSON.stringify({ question, postAnonymously, token }),
120
120
  }),
121
- transformResponse: (response, meta, arg) => {
121
+ transformResponse: (response) => {
122
122
  return response.data;
123
123
  },
124
124
  }),
@@ -0,0 +1,13 @@
1
+ /*
2
+ * Remove special characters and replace spaces with underscores
3
+ * @param {string} str - The input string to be cleaned
4
+ * @returns {string} - The cleaned string with special characters removed and spaces replaced with underscores
5
+ */
6
+ export function cleanString(str) {
7
+ return str
8
+ .normalize('NFD') // Decompose accented characters
9
+ .replace(/[\u0300-\u036f]/g, '') // Remove diacritics
10
+ .replace(/[^a-zA-Z0-9 ]/g, '') // Remove non-ASCII letters, numbers, and spaces
11
+ .trim() // Trim leading/trailing spaces
12
+ .replace(/\s+/g, '_'); // Replace spaces with underscores
13
+ }
@@ -0,0 +1,7 @@
1
+ export const trackEvent = (trackingData) => {
2
+ const event = new CustomEvent('oTracking.event', {
3
+ detail: trackingData,
4
+ bubbles: true,
5
+ });
6
+ document.body.dispatchEvent(event);
7
+ };
package/dist/qanda.scss CHANGED
@@ -339,6 +339,7 @@ $app-header-color: #007acc;
339
339
  from {
340
340
  transform: translateY(100%);
341
341
  }
342
+
342
343
  to {
343
344
  transform: translateY(44px);
344
345
  }
@@ -348,6 +349,7 @@ $app-header-color: #007acc;
348
349
  from {
349
350
  transform: translateX(100%);
350
351
  }
352
+
351
353
  to {
352
354
  transform: translateX(0);
353
355
  }
@@ -357,6 +359,7 @@ $app-header-color: #007acc;
357
359
  from {
358
360
  opacity: 0;
359
361
  }
362
+
360
363
  to {
361
364
  opacity: 0.4;
362
365
  }
@@ -480,7 +483,7 @@ $app-header-color: #007acc;
480
483
 
481
484
  .overlay__submit-button {
482
485
  width: 100%;
483
- margin-bottom: var(--o3-spacing-4xs);
486
+ margin: var(--o3-spacing-s) 0 var(--o3-spacing-4xs) 0;
484
487
  }
485
488
 
486
489
  .overlay__user-container-display-name {
@@ -497,22 +500,6 @@ $app-header-color: #007acc;
497
500
  mask-repeat: no-repeat;
498
501
  mask-size: contain;
499
502
  }
500
-
501
- .message-box__container {
502
- width: 100%;
503
- text-align: left;
504
- }
505
-
506
- .message-box__content {
507
- display: flex;
508
- flex-direction: column;
509
- gap: var(--o3-spacing-4xs);
510
- }
511
-
512
- .message-box__content-link {
513
- color: var(--o3-color-use-case-body-text);
514
- text-decoration-color: var(--o3-color-use-case-body-text);
515
- }
516
503
  }
517
504
 
518
505
  /* Inlined from ../components/QandaBlock/styles.css */
@@ -674,6 +661,10 @@ $app-header-color: #007acc;
674
661
  color: var(--o3-color-use-case-error, #c00);
675
662
  }
676
663
  }
664
+
665
+ .question-form__question__input {
666
+ margin-bottom: var(--o3-spacing-4xs, 8px);
667
+ }
677
668
  }
678
669
 
679
670
  /* Inlined from ../components/Stream/styles.css */
@@ -702,6 +693,27 @@ $app-header-color: #007acc;
702
693
  }
703
694
  }
704
695
 
696
+ /* Inlined from ../components/MessageBox/styles.css */
697
+ @import '@financial-times/o3-button/css/core.css';
698
+
699
+ .qanda-container {
700
+ .message-box__container {
701
+ width: 100%;
702
+ text-align: left;
703
+ }
704
+
705
+ .message-box__content {
706
+ display: flex;
707
+ flex-direction: column;
708
+ gap: var(--o3-spacing-4xs);
709
+ }
710
+
711
+ .message-box__content-link {
712
+ color: var(--o3-color-use-case-body-text);
713
+ text-decoration-color: var(--o3-color-use-case-body-text);
714
+ }
715
+ }
716
+
705
717
 
706
718
  @include oForms(
707
719
  $opts: (
@@ -741,6 +753,18 @@ $app-header-color: #007acc;
741
753
  )
742
754
  );
743
755
 
756
+ @include oMessageAddState(
757
+ $name: 'survey',
758
+ $opts: (
759
+ 'background-color': 'o3-color-palette-black-5',
760
+ 'foreground-color': 'o3-color-palette-slate',
761
+ 'text-align': 'left',
762
+ ),
763
+ $types: (
764
+ 'action',
765
+ )
766
+ );
767
+
744
768
  .qanda-container {
745
769
  body {
746
770
  margin: 0;
@@ -0,0 +1,29 @@
1
+ import type { FetchBaseQueryError } from '@reduxjs/toolkit/query';
2
+ import type { SerializedError } from '@reduxjs/toolkit';
3
+ import type { ErrorObjectResponseWithUserErrorCode } from '../../types/comments-api';
4
+ type AbortError = {
5
+ name: 'AbortError';
6
+ message: string;
7
+ };
8
+ type UnMappedErrorDetails = {
9
+ name: string;
10
+ message: string;
11
+ };
12
+ export type ErrorMessage = Record<string, {
13
+ title: string;
14
+ message: string;
15
+ }>;
16
+ type FetchBaseQueryErrorWithUserErrorCode = {
17
+ status: number;
18
+ data: ErrorObjectResponseWithUserErrorCode;
19
+ };
20
+ export type AcceptedError = FetchBaseQueryError | SerializedError | AbortError | FetchBaseQueryErrorWithUserErrorCode;
21
+ export default class HumanReadableError {
22
+ private _humanReadableError;
23
+ private _unMappedError;
24
+ constructor(error: AcceptedError, customErrorMessages?: ErrorMessage);
25
+ get title(): string;
26
+ get message(): string;
27
+ get unmappedError(): UnMappedErrorDetails | null;
28
+ }
29
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { AcceptedError, ErrorMessage } from './humanReadableError';
2
+ export default function ErrorMessage({ error, storyId, errorMapping, operation, id, }: {
3
+ error: AcceptedError;
4
+ storyId: string;
5
+ errorMapping?: ErrorMessage;
6
+ operation?: string;
7
+ id?: string;
8
+ }): import("react").JSX.Element;
@@ -1,9 +1,10 @@
1
1
  import { JSX } from 'react';
2
2
  type MessageBoxProps = {
3
3
  color?: string;
4
- styles: string;
5
4
  text: string | JSX.Element;
6
5
  title: string;
6
+ extraClassNames?: string;
7
+ id?: string;
7
8
  };
8
- declare function MessageBox({ color, styles, text, title }: MessageBoxProps): JSX.Element;
9
+ declare function MessageBox({ color, extraClassNames, text, title, id, }: MessageBoxProps): JSX.Element;
9
10
  export default MessageBox;
@@ -0,0 +1,6 @@
1
+ type SurveyProps = {
2
+ storyId: string;
3
+ status: string;
4
+ };
5
+ declare function Survey({ storyId, status }: SurveyProps): import("preact").JSX.Element;
6
+ export default Survey;
@@ -38,11 +38,15 @@ export declare const nextCommentsApi: import("@reduxjs/toolkit/query/react").Api
38
38
  }, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, CommentsStreamTransformed, "nextCommentsApi">;
39
39
  getQandAUpdates: import("@reduxjs/toolkit/query/react").QueryDefinition<CommentsAPIUrlParams, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, CachedData, "nextCommentsApi">;
40
40
  getUserRoles: import("@reduxjs/toolkit/query/react").QueryDefinition<CommentsAPIUrlTokenParams, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, UserRolesTransformed, "nextCommentsApi">;
41
- postQuestion: import("@reduxjs/toolkit/query/react").MutationDefinition<CommentsAPIUrlQuestionParams, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, any, "nextCommentsApi">;
41
+ postQuestion: import("@reduxjs/toolkit/query/react").MutationDefinition<CommentsAPIUrlQuestionParams, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, {
42
+ message: string;
43
+ }, "nextCommentsApi">;
42
44
  }, "nextCommentsApi", never, typeof import("@reduxjs/toolkit/query/react").coreModuleName | typeof import("@reduxjs/toolkit/query/react").reactHooksModuleName>;
43
45
  export declare const useGetQandAStreamQuery: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseQuery<import("@reduxjs/toolkit/query/react").QueryDefinition<CommentsAPIUrlParams & {
44
46
  status: Status;
45
- }, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, CommentsStreamTransformed, "nextCommentsApi">>, useGetQandAUpdatesQuery: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseQuery<import("@reduxjs/toolkit/query/react").QueryDefinition<CommentsAPIUrlParams, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, CachedData, "nextCommentsApi">>, useGetUserRolesQuery: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseQuery<import("@reduxjs/toolkit/query/react").QueryDefinition<CommentsAPIUrlTokenParams, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, UserRolesTransformed, "nextCommentsApi">>, usePostQuestionMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query/react").MutationDefinition<CommentsAPIUrlQuestionParams, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, any, "nextCommentsApi">>;
47
+ }, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, CommentsStreamTransformed, "nextCommentsApi">>, useGetQandAUpdatesQuery: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseQuery<import("@reduxjs/toolkit/query/react").QueryDefinition<CommentsAPIUrlParams, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, CachedData, "nextCommentsApi">>, useGetUserRolesQuery: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseQuery<import("@reduxjs/toolkit/query/react").QueryDefinition<CommentsAPIUrlTokenParams, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, UserRolesTransformed, "nextCommentsApi">>, usePostQuestionMutation: import("@reduxjs/toolkit/dist/query/react/buildHooks").UseMutation<import("@reduxjs/toolkit/query/react").MutationDefinition<CommentsAPIUrlQuestionParams, import("@reduxjs/toolkit/query/react").BaseQueryFn<string | import("@reduxjs/toolkit/query/react").FetchArgs, unknown, import("@reduxjs/toolkit/query/react").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query/react").FetchBaseQueryMeta>, never, {
48
+ message: string;
49
+ }, "nextCommentsApi">>;
46
50
  export declare const useUpdatedComments: ({ storyId, useStaging, commentsAPIUrl, }: CommentsAPIUrlParams) => Comment[];
47
51
  export declare const useUpdatedQA: ({ storyId, useStaging, commentsAPIUrl, }: CommentsAPIUrlParams) => any;
48
52
  export {};
@@ -51,7 +51,9 @@ export declare const rootReducer: import("@reduxjs/toolkit").Reducer<import("@re
51
51
  } & {
52
52
  question: string;
53
53
  postAnonymously: boolean;
54
- }, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@reduxjs/toolkit/query").FetchArgs, unknown, import("@reduxjs/toolkit/query").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query").FetchBaseQueryMeta>, never, any, "nextCommentsApi">;
54
+ }, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@reduxjs/toolkit/query").FetchArgs, unknown, import("@reduxjs/toolkit/query").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query").FetchBaseQueryMeta>, never, {
55
+ message: string;
56
+ }, "nextCommentsApi">;
55
57
  }, never, "nextCommentsApi">;
56
58
  }>, import("@reduxjs/toolkit").AnyAction>;
57
59
  export type RootState = {
@@ -108,7 +110,9 @@ export declare const store: import("@reduxjs/toolkit/dist/configureStore").Toolk
108
110
  } & {
109
111
  question: string;
110
112
  postAnonymously: boolean;
111
- }, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@reduxjs/toolkit/query").FetchArgs, unknown, import("@reduxjs/toolkit/query").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query").FetchBaseQueryMeta>, never, any, "nextCommentsApi">;
113
+ }, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@reduxjs/toolkit/query").FetchArgs, unknown, import("@reduxjs/toolkit/query").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query").FetchBaseQueryMeta>, never, {
114
+ message: string;
115
+ }, "nextCommentsApi">;
112
116
  }, never, "nextCommentsApi">;
113
117
  }, import("@reduxjs/toolkit").AnyAction, import("@reduxjs/toolkit").MiddlewareArray<[import("@reduxjs/toolkit").ThunkMiddleware<import("@reduxjs/toolkit").CombinedState<{
114
118
  stream: any;
@@ -158,7 +162,9 @@ export declare const store: import("@reduxjs/toolkit/dist/configureStore").Toolk
158
162
  } & {
159
163
  question: string;
160
164
  postAnonymously: boolean;
161
- }, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@reduxjs/toolkit/query").FetchArgs, unknown, import("@reduxjs/toolkit/query").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query").FetchBaseQueryMeta>, never, any, "nextCommentsApi">;
165
+ }, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@reduxjs/toolkit/query").FetchArgs, unknown, import("@reduxjs/toolkit/query").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query").FetchBaseQueryMeta>, never, {
166
+ message: string;
167
+ }, "nextCommentsApi">;
162
168
  }, never, "nextCommentsApi">;
163
169
  }>, import("@reduxjs/toolkit").AnyAction>, import("@reduxjs/toolkit").Middleware<{}, import("@reduxjs/toolkit/query").RootState<{
164
170
  getQandAStream: import("@reduxjs/toolkit/query").QueryDefinition<{
@@ -203,7 +209,9 @@ export declare const store: import("@reduxjs/toolkit/dist/configureStore").Toolk
203
209
  } & {
204
210
  question: string;
205
211
  postAnonymously: boolean;
206
- }, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@reduxjs/toolkit/query").FetchArgs, unknown, import("@reduxjs/toolkit/query").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query").FetchBaseQueryMeta>, never, any, "nextCommentsApi">;
212
+ }, import("@reduxjs/toolkit/query").BaseQueryFn<string | import("@reduxjs/toolkit/query").FetchArgs, unknown, import("@reduxjs/toolkit/query").FetchBaseQueryError, {}, import("@reduxjs/toolkit/query").FetchBaseQueryMeta>, never, {
213
+ message: string;
214
+ }, "nextCommentsApi">;
207
215
  }, string, "nextCommentsApi">, ThunkDispatch<any, any, import("@reduxjs/toolkit").AnyAction>>, import("@reduxjs/toolkit").Middleware<{}, any, import("@reduxjs/toolkit").Dispatch<Action<any>>>]>>;
208
216
  export type AnyAction = typeof store.dispatch;
209
217
  export type AppDispatch = ThunkDispatch<RootState, unknown, any>;
@@ -0,0 +1 @@
1
+ export declare function cleanString(str: string): string;
@@ -0,0 +1,2 @@
1
+ import type { TrackingData } from '../types/tracking';
2
+ export declare const trackEvent: (trackingData: TrackingData) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/qanda-ui",
3
- "version": "0.0.1-beta.19",
3
+ "version": "0.0.1-beta.20",
4
4
  "description": "Components for the Live Q&A UI",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -100,13 +100,13 @@
100
100
  "@financial-times/cp-content-pipeline-ui": "^9.2.1",
101
101
  "@financial-times/n-tracking": "^7.7.0",
102
102
  "@financial-times/o-colors": "^6.7",
103
- "@financial-times/o-comments": "^13.0.0",
103
+ "@financial-times/o-comments": "^13.0.1",
104
104
  "@financial-times/o-forms": "^10.0.2",
105
105
  "@financial-times/o-icons": "^7.8",
106
106
  "@financial-times/o-labels": "^7.0.1",
107
107
  "@financial-times/o-loading": "^6.0.0",
108
108
  "@financial-times/o-message": "^6.0.0",
109
- "@financial-times/o-tracking": "^4.8.0",
109
+ "@financial-times/o-tracking": "^4.9.0",
110
110
  "@financial-times/o3-button": "^3.7.0",
111
111
  "@financial-times/o3-foundation": "^3.2",
112
112
  "preact": "^10.6.6",
@@ -1,7 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const jsx_runtime_1 = require("preact/jsx-runtime");
4
- function MessageBox({ color, styles, text, title }) {
5
- return ((0, jsx_runtime_1.jsx)("div", { class: `o-message o-message--inner ${styles} message-box__container`, "data-o-message-close": "false", "data-o-component": "o-message", 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;
@@ -1,22 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const jsx_runtime_1 = require("preact/jsx-runtime");
4
- const react_1 = require("react");
5
- const react_redux_1 = require("react-redux");
6
- const comments_api_1 = require("../services/comments-api");
7
- const QandaProvider_1 = require("./QandaProvider");
8
- function UserInfo() {
9
- const profile = (0, react_redux_1.useSelector)((state) => state.user.profile);
10
- if (!profile) {
11
- return null;
12
- }
13
- const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
14
- const { data: userRole, error, isLoading, } = (0, comments_api_1.useGetUserRolesQuery)({
15
- storyId,
16
- useStaging,
17
- commentsAPIUrl,
18
- token: profile.token,
19
- }, { skip: !profile.token });
20
- return ((0, jsx_runtime_1.jsxs)("div", { children: [isLoading && (0, jsx_runtime_1.jsx)("p", { children: "Loading your profile..." }), error && ((0, jsx_runtime_1.jsx)("p", { children: (0, jsx_runtime_1.jsx)("b", { children: error }) }))] }));
21
- }
22
- exports.default = UserInfo;
@@ -1,5 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- function MessageBox({ color, styles, text, title }) {
3
- return (_jsx("div", { class: `o-message o-message--inner ${styles} message-box__container`, "data-o-message-close": "false", "data-o-component": "o-message", children: _jsx("div", { class: "o-message__container", children: _jsxs("div", { class: "message-box__content", children: [_jsx("span", { style: { color }, class: "o3-type-body-highlight", children: title }), _jsx("span", { style: { color }, class: "o3-type-body-base", children: text })] }) }) }));
4
- }
5
- export default MessageBox;
@@ -1,20 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- import { useContext } from 'react';
3
- import { useSelector } from 'react-redux';
4
- import { useGetUserRolesQuery } from '../services/comments-api';
5
- import { QandaContext } from './QandaProvider';
6
- function UserInfo() {
7
- const profile = useSelector((state) => state.user.profile);
8
- if (!profile) {
9
- return null;
10
- }
11
- const { storyId, useStaging, commentsAPIUrl } = useContext(QandaContext);
12
- const { data: userRole, error, isLoading, } = useGetUserRolesQuery({
13
- storyId,
14
- useStaging,
15
- commentsAPIUrl,
16
- token: profile.token,
17
- }, { skip: !profile.token });
18
- return (_jsxs("div", { children: [isLoading && _jsx("p", { children: "Loading your profile..." }), error && (_jsx("p", { children: _jsx("b", { children: error }) }))] }));
19
- }
20
- export default UserInfo;
@@ -1,2 +0,0 @@
1
- declare function UserInfo(): import("react").JSX.Element | null;
2
- export default UserInfo;