@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.
- package/dist/cjs/components/ErrorMessage/humanReadableError.js +74 -0
- package/dist/cjs/components/ErrorMessage/index.js +63 -0
- package/dist/cjs/components/MessageBox/index.js +7 -0
- package/dist/cjs/components/QandaContainer.js +24 -5
- package/dist/cjs/components/QuestionForm/index.js +36 -27
- package/dist/cjs/components/Stream/index.js +2 -1
- package/dist/cjs/components/Survey/index.js +36 -0
- package/dist/cjs/services/comments-api.js +1 -1
- package/dist/cjs/utils/strings.js +16 -0
- package/dist/cjs/utils/tracking.js +11 -0
- package/dist/esm/components/ErrorMessage/humanReadableError.js +71 -0
- package/dist/esm/components/ErrorMessage/index.js +57 -0
- package/dist/esm/components/MessageBox/index.js +5 -0
- package/dist/esm/components/QandaContainer.js +25 -6
- package/dist/esm/components/QuestionForm/index.js +35 -26
- package/dist/esm/components/Stream/index.js +2 -1
- package/dist/esm/components/Survey/index.js +34 -0
- package/dist/esm/services/comments-api.js +2 -2
- package/dist/esm/utils/strings.js +13 -0
- package/dist/esm/utils/tracking.js +7 -0
- package/dist/qanda.scss +41 -17
- package/dist/types/components/ErrorMessage/humanReadableError.d.ts +29 -0
- package/dist/types/components/ErrorMessage/index.d.ts +8 -0
- package/dist/types/components/{Overlay/MessageBox.d.ts → MessageBox/index.d.ts} +3 -2
- package/dist/types/components/Survey/index.d.ts +6 -0
- package/dist/types/services/comments-api.d.ts +6 -2
- package/dist/types/store/index.d.ts +12 -4
- package/dist/types/utils/strings.d.ts +1 -0
- package/dist/types/utils/tracking.d.ts +2 -0
- package/package.json +3 -3
- package/dist/cjs/components/Overlay/MessageBox.js +0 -7
- package/dist/cjs/components/UserInfo.js +0 -22
- package/dist/esm/components/Overlay/MessageBox.js +0 -5
- package/dist/esm/components/UserInfo.js +0 -20
- 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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 }),
|
|
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("../
|
|
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-
|
|
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",
|
|
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)(
|
|
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
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 }),
|
|
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 '../
|
|
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-
|
|
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",
|
|
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(
|
|
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
|
|
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
|
|
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
|
+
}
|
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
|
|
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,
|
|
9
|
+
declare function MessageBox({ color, extraClassNames, text, title, id, }: MessageBoxProps): JSX.Element;
|
|
9
10
|
export default MessageBox;
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@financial-times/qanda-ui",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
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.
|
|
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.
|
|
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;
|