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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +38 -6
  2. package/dist/cjs/components/Author/index.js +19 -0
  3. package/dist/cjs/components/Calendar/Apps.js +48 -0
  4. package/dist/cjs/components/Calendar/CalendarList.js +12 -0
  5. package/dist/cjs/components/Calendar/CalendarListItem.js +24 -0
  6. package/dist/cjs/components/Calendar/index.js +47 -0
  7. package/dist/cjs/components/CountdownTimer/index.js +91 -0
  8. package/dist/cjs/components/EmbedFormField/index.js +24 -0
  9. package/dist/cjs/components/ErrorMessage/humanReadableError.js +74 -0
  10. package/dist/cjs/components/ErrorMessage/index.js +63 -0
  11. package/dist/cjs/components/Expander/index.js +71 -0
  12. package/dist/cjs/components/ExpertDrawer/index.js +36 -0
  13. package/dist/cjs/components/FloatingActionBar/index.js +59 -0
  14. package/dist/cjs/components/Loader/index.js +7 -0
  15. package/dist/cjs/components/MessageBox/index.js +7 -0
  16. package/dist/cjs/components/Overlay/index.js +18 -0
  17. package/dist/cjs/components/Qanda.js +37 -0
  18. package/dist/cjs/components/QandaBlock/index.js +78 -0
  19. package/dist/cjs/components/QandaContainer.js +99 -0
  20. package/dist/cjs/components/QandaProvider.js +11 -0
  21. package/dist/cjs/components/QuestionForm/index.js +165 -0
  22. package/dist/cjs/components/QuestionForm/messagesByUserType.js +33 -0
  23. package/dist/cjs/components/Stream/index.js +37 -0
  24. package/dist/cjs/components/Survey/index.js +36 -0
  25. package/dist/cjs/hooks/useIsMobile.js +15 -0
  26. package/dist/cjs/index.js +53 -0
  27. package/dist/cjs/services/comments-api.js +152 -0
  28. package/dist/cjs/services/error-middleware.js +47 -0
  29. package/dist/cjs/services/reliability.js +79 -0
  30. package/dist/cjs/store/expertSlice.js +26 -0
  31. package/dist/cjs/store/index.js +30 -0
  32. package/dist/cjs/store/questionSlice.js +32 -0
  33. package/dist/cjs/store/stream.js +17 -0
  34. package/dist/cjs/store/updateComments.js +40 -0
  35. package/dist/cjs/store/user.js +51 -0
  36. package/dist/cjs/utils/auth.js +26 -0
  37. package/dist/cjs/utils/qandas.js +20 -0
  38. package/dist/cjs/utils/strings.js +16 -0
  39. package/dist/cjs/utils/tracking.js +11 -0
  40. package/dist/cjs/utils/validate-display-name.js +22 -0
  41. package/dist/esm/components/Author/index.js +14 -0
  42. package/dist/esm/components/Calendar/Apps.js +46 -0
  43. package/dist/esm/components/Calendar/CalendarList.js +7 -0
  44. package/dist/esm/components/Calendar/CalendarListItem.js +22 -0
  45. package/dist/esm/components/Calendar/index.js +42 -0
  46. package/dist/esm/components/CountdownTimer/index.js +88 -0
  47. package/dist/esm/components/EmbedFormField/index.js +22 -0
  48. package/dist/esm/components/ErrorMessage/humanReadableError.js +71 -0
  49. package/dist/esm/components/ErrorMessage/index.js +57 -0
  50. package/dist/esm/components/Expander/index.js +69 -0
  51. package/dist/esm/components/ExpertDrawer/index.js +31 -0
  52. package/dist/esm/components/FloatingActionBar/index.js +57 -0
  53. package/dist/esm/components/Loader/index.js +4 -0
  54. package/dist/esm/components/MessageBox/index.js +5 -0
  55. package/dist/esm/components/Overlay/index.js +13 -0
  56. package/dist/esm/components/Qanda.js +32 -0
  57. package/dist/esm/components/QandaBlock/index.js +73 -0
  58. package/dist/esm/components/QandaContainer.js +94 -0
  59. package/dist/esm/components/QandaProvider.js +8 -0
  60. package/dist/esm/components/QuestionForm/index.js +159 -0
  61. package/dist/esm/components/QuestionForm/messagesByUserType.js +28 -0
  62. package/dist/esm/components/Stream/index.js +32 -0
  63. package/dist/esm/components/Survey/index.js +34 -0
  64. package/dist/esm/hooks/useIsMobile.js +12 -0
  65. package/dist/esm/index.js +46 -0
  66. package/dist/esm/services/comments-api.js +147 -0
  67. package/dist/esm/services/error-middleware.js +43 -0
  68. package/dist/esm/services/reliability.js +75 -0
  69. package/dist/esm/store/expertSlice.js +22 -0
  70. package/dist/esm/store/index.js +24 -0
  71. package/dist/esm/store/questionSlice.js +28 -0
  72. package/dist/esm/store/stream.js +14 -0
  73. package/dist/esm/store/updateComments.js +36 -0
  74. package/dist/esm/store/user.js +45 -0
  75. package/dist/esm/utils/auth.js +24 -0
  76. package/dist/esm/utils/qandas.js +16 -0
  77. package/dist/esm/utils/strings.js +13 -0
  78. package/dist/esm/utils/tracking.js +7 -0
  79. package/dist/esm/utils/validate-display-name.js +19 -0
  80. package/dist/qanda.scss +802 -0
  81. package/dist/types/components/Author/index.d.ts +18 -0
  82. package/dist/types/components/Calendar/Apps.d.ts +14 -0
  83. package/dist/types/components/Calendar/CalendarList.d.ts +2 -0
  84. package/dist/types/components/Calendar/CalendarListItem.d.ts +8 -0
  85. package/dist/types/components/Calendar/index.d.ts +2 -0
  86. package/dist/types/components/CountdownTimer/index.d.ts +6 -0
  87. package/dist/types/components/EmbedFormField/index.d.ts +5 -0
  88. package/dist/types/components/ErrorMessage/humanReadableError.d.ts +29 -0
  89. package/dist/types/components/ErrorMessage/index.d.ts +8 -0
  90. package/dist/types/components/Expander/index.d.ts +8 -0
  91. package/dist/types/components/ExpertDrawer/index.d.ts +2 -0
  92. package/dist/types/components/FloatingActionBar/index.d.ts +4 -0
  93. package/dist/types/components/Loader/index.d.ts +14 -0
  94. package/dist/types/components/MessageBox/index.d.ts +10 -0
  95. package/dist/types/components/Overlay/index.d.ts +12 -0
  96. package/dist/types/components/Qanda.d.ts +3 -0
  97. package/dist/types/components/QandaBlock/index.d.ts +7 -0
  98. package/dist/types/components/QandaContainer.d.ts +6 -0
  99. package/dist/types/components/QandaProvider.d.ts +9 -0
  100. package/dist/types/components/QuestionForm/index.d.ts +1 -0
  101. package/dist/types/components/QuestionForm/messagesByUserType.d.ts +7 -0
  102. package/dist/types/components/Stream/index.d.ts +5 -0
  103. package/dist/types/components/Survey/index.d.ts +6 -0
  104. package/dist/types/hooks/useIsMobile.d.ts +1 -0
  105. package/dist/types/index.d.ts +17 -0
  106. package/dist/types/services/comments-api.d.ts +52 -0
  107. package/dist/types/services/error-middleware.d.ts +2 -0
  108. package/dist/types/services/reliability.d.ts +19 -0
  109. package/dist/types/store/expertSlice.d.ts +14 -0
  110. package/dist/types/store/index.d.ts +218 -0
  111. package/dist/types/store/questionSlice.d.ts +16 -0
  112. package/dist/types/store/stream.d.ts +7 -0
  113. package/dist/types/store/updateComments.d.ts +3 -0
  114. package/dist/types/store/user.d.ts +26 -0
  115. package/dist/types/utils/auth.d.ts +6 -0
  116. package/dist/types/utils/qandas.d.ts +2 -0
  117. package/dist/types/utils/strings.d.ts +1 -0
  118. package/dist/types/utils/tracking.d.ts +2 -0
  119. package/dist/types/utils/validate-display-name.d.ts +1 -0
  120. package/package.json +35 -29
  121. package/dist/index.html +0 -19
  122. package/dist/index.js +0 -2
  123. package/dist/index.js.LICENSE.txt +0 -15
@@ -0,0 +1,53 @@
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.QandaBlock = exports.QandaUI = void 0;
7
+ const jsx_runtime_1 = require("preact/jsx-runtime");
8
+ const preact_1 = require("preact");
9
+ const Qanda_1 = __importDefault(require("./components/Qanda"));
10
+ const QandaBlock_1 = __importDefault(require("./components/QandaBlock"));
11
+ exports.QandaBlock = QandaBlock_1.default;
12
+ class QandaUI {
13
+ constructor(containerElement, options) {
14
+ this.containerElement = containerElement;
15
+ this.embedFormFieldContainer =
16
+ options?.embedFormFieldContainer || document.body;
17
+ this.countdownTimerContainer =
18
+ options?.countdownTimerContainer || document.body;
19
+ if (!options && !containerElement) {
20
+ throw new Error('Required properties are missing');
21
+ }
22
+ const storyId = options?.storyId ?? containerElement.dataset.qandaStoryid;
23
+ const useStaging = options?.useStaging ??
24
+ containerElement.dataset.qandaUsestaging === 'true' ??
25
+ false;
26
+ const title = options?.title ?? containerElement.dataset.qandaTitle;
27
+ const commentsAPIHost = options?.commentsAPIHost ?? containerElement.dataset.qandaCommentsapihost;
28
+ const commentsAPIVersion = options?.commentsAPIVersion ??
29
+ containerElement.dataset.qandaCommentsapiversion;
30
+ const maxQuestionLength = options?.maxQuestionLength ??
31
+ (containerElement.dataset.qandaMaxquestionlength
32
+ ? parseInt(containerElement.dataset.qandaMaxquestionlength, 10)
33
+ : undefined) ??
34
+ 2000;
35
+ if (!storyId || !title || !commentsAPIHost || !commentsAPIVersion) {
36
+ throw new Error('Required properties are missing');
37
+ }
38
+ //TODO: test this doesn't break all the other bits of js, needs to be throw because of typescript, can't just bail out normally
39
+ this.storyId = storyId;
40
+ this.useStaging = useStaging;
41
+ this.title = title;
42
+ this.commentsAPIHost = commentsAPIHost;
43
+ this.commentsAPIVersion = commentsAPIVersion;
44
+ this.maxQuestionLength = maxQuestionLength;
45
+ }
46
+ init() {
47
+ (0, preact_1.render)((0, jsx_runtime_1.jsx)(Qanda_1.default, { storyId: this.storyId, useStaging: this.useStaging, title: this.title, commentsAPIHost: this.commentsAPIHost, commentsAPIVersion: this.commentsAPIVersion, maxQuestionLength: this.maxQuestionLength, embedFormFieldContainer: this.embedFormFieldContainer, countdownTimerContainer: this.countdownTimerContainer }), this.containerElement);
48
+ }
49
+ destroy() {
50
+ (0, preact_1.render)(null, this.containerElement);
51
+ }
52
+ }
53
+ exports.QandaUI = QandaUI;
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useUpdatedQA = exports.useUpdatedComments = exports.usePostQuestionMutation = exports.useGetUserRolesQuery = exports.useGetQandAUpdatesQuery = exports.useGetQandAStreamQuery = exports.nextCommentsApi = exports.eventSource = void 0;
4
+ const react_1 = require("@reduxjs/toolkit/query/react");
5
+ // TODO: ensure that users don't miss out on updates in this window
6
+ // https://financialtimes.atlassian.net/browse/CI-2715
7
+ const retryDelay = 1000;
8
+ const updateTypes = {
9
+ QA_NEW_REPLY: 'updatedComments',
10
+ QA_UPDATE: 'updatedQA',
11
+ };
12
+ exports.eventSource = null;
13
+ exports.nextCommentsApi = (0, react_1.createApi)({
14
+ reducerPath: 'nextCommentsApi',
15
+ refetchOnReconnect: true, // refetches all subscribed queries on regaining a network connection
16
+ baseQuery: (0, react_1.fetchBaseQuery)({
17
+ timeout: 10000,
18
+ }),
19
+ endpoints: (builder) => ({
20
+ // QA STREAM
21
+ getQandAStream: builder.query({
22
+ query: ({ storyId, useStaging, commentsAPIUrl, status, }) =>
23
+ // The usage of status is to create different caches (RTK and Fastly) for different statuses
24
+ `${commentsAPIUrl}/story/${storyId}/stream?status=${status}${useStaging ? '&staging=1' : ''}`,
25
+ transformResponse: (response) => {
26
+ return {
27
+ qandas: response?.children || [],
28
+ type: response.type,
29
+ status: response?.status || 'pre-live',
30
+ startDateTime: response?.startDateTime || '',
31
+ endDateTime: response?.endDateTime || '',
32
+ };
33
+ },
34
+ }),
35
+ // UPDATES LISTENER
36
+ getQandAUpdates: builder.query({
37
+ queryFn: async ({ storyId, useStaging, commentsAPIUrl, }) => {
38
+ return { data: {} };
39
+ },
40
+ async onCacheEntryAdded(arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }) {
41
+ await cacheDataLoaded;
42
+ const connectToSSE = () => {
43
+ const sseEndpoint = `${arg.commentsAPIUrl}/story/${arg.storyId}/updates${arg.useStaging ? '?staging=1' : ''}`;
44
+ exports.eventSource = new EventSource(sseEndpoint);
45
+ exports.eventSource.onopen = () => {
46
+ console.log(`Connected to SSE: ${sseEndpoint}`);
47
+ };
48
+ exports.eventSource.onmessage = (event) => {
49
+ try {
50
+ const data = JSON.parse(event.data);
51
+ console.log('Received SSE update:', data);
52
+ if (data && data.type) {
53
+ updateCachedData((draft) => {
54
+ const updatedData = updateTypes[data.type];
55
+ draft[updatedData] = draft[updatedData] || [];
56
+ draft[updatedData].push(data.payload);
57
+ });
58
+ }
59
+ }
60
+ catch (error) {
61
+ console.warn('Failed to parse SSE message:', event.data);
62
+ }
63
+ };
64
+ exports.eventSource.onerror = (event) => {
65
+ const es = event.target;
66
+ if (es.readyState === EventSource.CONNECTING) {
67
+ console.error('SSE connection lost. Attempting to reconnect...');
68
+ }
69
+ else if (es.readyState === EventSource.CLOSED) {
70
+ console.error(`SSE connection closed. Will try to reconnect in ${retryDelay / 1000} seconds...`);
71
+ exports.eventSource?.close();
72
+ exports.eventSource = null;
73
+ setTimeout(() => connectToSSE(), retryDelay);
74
+ }
75
+ else {
76
+ console.error('SSE error occurred.', event);
77
+ }
78
+ };
79
+ };
80
+ try {
81
+ connectToSSE();
82
+ }
83
+ catch (error) {
84
+ console.error('Failed to subscribe to SSE:', error);
85
+ console.error(`Reconnecting in ${retryDelay / 1000} seconds...`);
86
+ exports.eventSource = null;
87
+ setTimeout(() => connectToSSE(), retryDelay);
88
+ }
89
+ // cleanup when there are no subscribers to the cache anymore
90
+ await cacheEntryRemoved;
91
+ if (exports.eventSource !== null) {
92
+ exports.eventSource.close();
93
+ exports.eventSource = null;
94
+ }
95
+ },
96
+ }),
97
+ // USER ROLES
98
+ getUserRoles: builder.query({
99
+ query: ({ storyId, useStaging, commentsAPIUrl, token, }) => ({
100
+ url: `${commentsAPIUrl}/user/user-details/story/${storyId}${useStaging ? '?staging=1' : ''}`,
101
+ method: 'POST',
102
+ headers: {
103
+ Authorization: `Bearer ${token}`,
104
+ },
105
+ }),
106
+ transformResponse: (response, meta, arg) => {
107
+ const allowedRoles = ['ADMIN', 'MODERATOR'];
108
+ const { isExpert, isStaffExpert, role } = response;
109
+ const expertView = isExpert || isStaffExpert || !!(role && allowedRoles.includes(role));
110
+ return { expertView };
111
+ },
112
+ }),
113
+ // POST A QUESTION
114
+ postQuestion: builder.mutation({
115
+ query: ({ storyId, useStaging, commentsAPIUrl, token, question, postAnonymously, }) => ({
116
+ url: `${commentsAPIUrl}/story/${storyId}/question${useStaging ? '?staging=1' : ''}`,
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ Authorization: `Bearer ${token}`,
121
+ },
122
+ body: JSON.stringify({ question, postAnonymously, token }),
123
+ }),
124
+ transformResponse: (response) => {
125
+ return response.data;
126
+ },
127
+ }),
128
+ }),
129
+ });
130
+ exports.useGetQandAStreamQuery = exports.nextCommentsApi.useGetQandAStreamQuery, exports.useGetQandAUpdatesQuery = exports.nextCommentsApi.useGetQandAUpdatesQuery, exports.useGetUserRolesQuery = exports.nextCommentsApi.useGetUserRolesQuery, exports.usePostQuestionMutation = exports.nextCommentsApi.usePostQuestionMutation;
131
+ // UPDATES QUERY CUSTOM HOOKS
132
+ // custom hook for getting just the updated comments from the updates, so we are more efficient
133
+ const useUpdatedComments = ({ storyId, useStaging, commentsAPIUrl, }) => {
134
+ return exports.nextCommentsApi.useGetQandAUpdatesQuery({ storyId, useStaging, commentsAPIUrl }, {
135
+ selectFromResult: ({ data }) => ({
136
+ updatedComments: data?.updatedComments || [],
137
+ }),
138
+ }).updatedComments;
139
+ };
140
+ exports.useUpdatedComments = useUpdatedComments;
141
+ // Custom hook to get the updates on the information about the Q&A
142
+ const useUpdatedQA = ({ storyId, useStaging, commentsAPIUrl, }) => {
143
+ return exports.nextCommentsApi.useGetQandAUpdatesQuery({ storyId, useStaging, commentsAPIUrl }, {
144
+ selectFromResult: ({ data }) => ({
145
+ updatedQA: (data?.updatedQA &&
146
+ data.updatedQA.length &&
147
+ data.updatedQA[data.updatedQA.length - 1]) ||
148
+ {},
149
+ }),
150
+ }).updatedQA;
151
+ };
152
+ exports.useUpdatedQA = useUpdatedQA;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rtkQueryErrorLogger = void 0;
4
+ const toolkit_1 = require("@reduxjs/toolkit");
5
+ const reliability_1 = require("./reliability");
6
+ // Tracking successes for updates may generate a lot of events
7
+ // Disable it when not needed
8
+ const endpointToOperationMap = {
9
+ postQuestion: { operation: 'question-submission', trackSuccess: true },
10
+ getUserRoles: { operation: 'profile-load', trackSuccess: true },
11
+ getQandAStream: { operation: 'stream-load', trackSuccess: true },
12
+ getQandAUpdates: { operation: 'qa-updates', trackSuccess: true },
13
+ };
14
+ const rtkQueryErrorLogger = () => (next) => (action) => {
15
+ // Log failures
16
+ if ((0, toolkit_1.isRejectedWithValue)(action)) {
17
+ const rejected = action;
18
+ const isHttpError = typeof rejected.payload?.status === 'number';
19
+ const endpointName = rejected.meta?.arg?.endpointName ?? '';
20
+ const mapEntry = endpointToOperationMap[endpointName];
21
+ const operation = mapEntry?.operation ?? 'unmapped-operation';
22
+ const statusCodeInfo = isHttpError
23
+ ? ` with status code ${rejected.payload?.status}`
24
+ : '';
25
+ const errorMessage = rejected.payload?.data?.error ||
26
+ rejected.payload?.error ||
27
+ 'Unknown error';
28
+ const rtkNormalisedError = new Error(`Network error${statusCodeInfo}: ${errorMessage}`);
29
+ reliability_1.qandaReliability.failure(operation, rtkNormalisedError);
30
+ }
31
+ // Fullfilled actions
32
+ if (typeof action === 'object' &&
33
+ action !== null &&
34
+ 'type' in action &&
35
+ typeof action.type === 'string' &&
36
+ action.type.endsWith('/fulfilled')) {
37
+ const meta = action.meta;
38
+ const endpointName = meta?.arg?.endpointName ?? '';
39
+ const mapEntry = endpointToOperationMap[endpointName];
40
+ // Send success event only if trackSucces is enabled for that particular endpoint
41
+ if (mapEntry?.trackSuccess) {
42
+ reliability_1.qandaReliability.success(mapEntry.operation);
43
+ }
44
+ }
45
+ return next(action);
46
+ };
47
+ exports.rtkQueryErrorLogger = rtkQueryErrorLogger;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Reliability = exports.qandaReliability = void 0;
4
+ /*
5
+ * @param outcome - success or failure
6
+ * @param operation - initialisation or question-submission
7
+ * @returns void
8
+ *
9
+ * This class is used to dispatch events to the client-metrics-web library
10
+ * for tracking the success or failure of operations.
11
+ * The class checks if the client-metrics-web library is available, if not it will log warnings
12
+ *
13
+ * For failures, it will log errors for debugging purposes
14
+ */
15
+ class Reliability {
16
+ constructor(options) {
17
+ this.testOnly = options.testOnly || false;
18
+ this.topLevelNamespace = options.topLevelNamespace;
19
+ this.articleId = '';
20
+ }
21
+ setArticleId(articleId) {
22
+ this.articleId = articleId;
23
+ }
24
+ dispatch(outcome, operation, error = null) {
25
+ if (!this.articleId) {
26
+ console.warn('(Qanda Reliability) Article ID not set. Please call setArticleId() before dispatching events.');
27
+ }
28
+ const eventNamespace = `${this.topLevelNamespace}.${operation}.${outcome}`;
29
+ if (this.testOnly) {
30
+ console.warn(`(Reliability) Event not sent: client-metrics-web not available %c${eventNamespace}`, 'font-weight: bold;');
31
+ }
32
+ else {
33
+ window.dispatchEvent(new CustomEvent('ft.clientMetric', {
34
+ detail: {
35
+ namespace: eventNamespace,
36
+ operation,
37
+ articleId: this.articleId,
38
+ ...(error && {
39
+ error: {
40
+ name: error?.name,
41
+ message: error?.message,
42
+ stack: error?.stack,
43
+ },
44
+ }),
45
+ },
46
+ }));
47
+ }
48
+ }
49
+ normaliseError(error) {
50
+ let normalisedError;
51
+ if (error instanceof Error) {
52
+ normalisedError = error;
53
+ }
54
+ else if (typeof error === 'string') {
55
+ normalisedError = new Error(error);
56
+ }
57
+ else if (typeof error === 'object' && error !== null) {
58
+ normalisedError = new Error(JSON.stringify(error));
59
+ }
60
+ else {
61
+ normalisedError = new Error('Unknown error type: ' + JSON.stringify(error));
62
+ }
63
+ return normalisedError;
64
+ }
65
+ failure(operation, error) {
66
+ // Dispatch a metric for failure and log the error
67
+ this.dispatch('failure', operation, this.normaliseError(error));
68
+ }
69
+ success(operation) {
70
+ this.dispatch('success', operation);
71
+ }
72
+ // Use this method to test the error handling
73
+ throw(error = 'Throwing an error for testing purposes') {
74
+ throw new Error(error);
75
+ }
76
+ }
77
+ exports.Reliability = Reliability;
78
+ const qandaReliability = new Reliability({ topLevelNamespace: 'live-qanda' });
79
+ exports.qandaReliability = qandaReliability;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var _a;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.setExpertView = exports.closeExpertForm = exports.openExpertForm = exports.expertSlice = void 0;
5
+ const toolkit_1 = require("@reduxjs/toolkit");
6
+ const initialState = {
7
+ isExpertFormOpen: false,
8
+ expertView: undefined,
9
+ };
10
+ exports.expertSlice = (0, toolkit_1.createSlice)({
11
+ name: 'expert',
12
+ initialState,
13
+ reducers: {
14
+ openExpertForm: (state) => {
15
+ state.isExpertFormOpen = true;
16
+ },
17
+ closeExpertForm: (state) => {
18
+ state.isExpertFormOpen = false;
19
+ },
20
+ setExpertView: (state, action) => {
21
+ state.expertView = action.payload;
22
+ },
23
+ },
24
+ });
25
+ _a = exports.expertSlice.actions, exports.openExpertForm = _a.openExpertForm, exports.closeExpertForm = _a.closeExpertForm, exports.setExpertView = _a.setExpertView;
26
+ exports.default = exports.expertSlice.reducer;
@@ -0,0 +1,30 @@
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.store = exports.rootReducer = void 0;
7
+ const toolkit_1 = require("@reduxjs/toolkit");
8
+ const query_1 = require("@reduxjs/toolkit/query");
9
+ const questionSlice_1 = __importDefault(require("./questionSlice"));
10
+ const stream_1 = require("./stream");
11
+ const user_1 = require("./user");
12
+ const expertSlice_1 = __importDefault(require("./expertSlice"));
13
+ const comments_api_1 = require("../services/comments-api");
14
+ const error_middleware_1 = require("../services/error-middleware");
15
+ exports.rootReducer = (0, toolkit_1.combineReducers)({
16
+ stream: stream_1.stream.reducer,
17
+ question: questionSlice_1.default,
18
+ user: user_1.user.reducer,
19
+ expert: expertSlice_1.default,
20
+ nextCommentsApi: comments_api_1.nextCommentsApi.reducer,
21
+ });
22
+ exports.store = (0, toolkit_1.configureStore)({
23
+ reducer: exports.rootReducer,
24
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware()
25
+ .concat(comments_api_1.nextCommentsApi.middleware)
26
+ .concat(error_middleware_1.rtkQueryErrorLogger),
27
+ });
28
+ // optional, but required for refetchOnFocus/refetchOnReconnect behaviors
29
+ // see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
30
+ (0, query_1.setupListeners)(exports.store.dispatch);
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var _a;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.updateDisplayNameError = exports.updateQuestion = exports.closeForm = exports.openForm = exports.questionSlice = void 0;
5
+ const toolkit_1 = require("@reduxjs/toolkit");
6
+ const initialState = {
7
+ isFormOpen: false,
8
+ question: '',
9
+ displayNameError: null,
10
+ };
11
+ exports.questionSlice = (0, toolkit_1.createSlice)({
12
+ name: 'question',
13
+ initialState,
14
+ reducers: {
15
+ openForm: (state) => {
16
+ state.isFormOpen = true;
17
+ },
18
+ closeForm: (state) => {
19
+ state.isFormOpen = false;
20
+ state.question = '';
21
+ state.displayNameError = null;
22
+ },
23
+ updateQuestion: (state, action) => {
24
+ state.question = action.payload;
25
+ },
26
+ updateDisplayNameError: (state, action) => {
27
+ state.displayNameError = action.payload;
28
+ },
29
+ },
30
+ });
31
+ _a = exports.questionSlice.actions, exports.openForm = _a.openForm, exports.closeForm = _a.closeForm, exports.updateQuestion = _a.updateQuestion, exports.updateDisplayNameError = _a.updateDisplayNameError;
32
+ exports.default = exports.questionSlice.reducer;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setLatestAnsweredQuestionId = exports.stream = exports.initialState = void 0;
4
+ const toolkit_1 = require("@reduxjs/toolkit");
5
+ exports.initialState = {
6
+ latestAnsweredQuestionId: '',
7
+ };
8
+ exports.stream = (0, toolkit_1.createSlice)({
9
+ name: 'stream',
10
+ initialState: exports.initialState,
11
+ reducers: {
12
+ setLatestAnsweredQuestionId: (state, action) => {
13
+ state.latestAnsweredQuestionId = action.payload;
14
+ },
15
+ },
16
+ });
17
+ exports.setLatestAnsweredQuestionId = exports.stream.actions.setLatestAnsweredQuestionId;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ // PURPOSE: move the comments saved in updatedComments into the main comments array for the stream and then clear out the updatedComments array, and update the lastAnsweredQuestionId in the store
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.updateComments = void 0;
5
+ const comments_api_1 = require("../services/comments-api");
6
+ const stream_1 = require("../store/stream");
7
+ const qandas_1 = require("../utils/qandas");
8
+ const updateComments = (dispatch, updatedComments, storyId, useStaging, commentsAPIUrl) => {
9
+ if (!updatedComments.length) {
10
+ return;
11
+ }
12
+ const latestAnsweredQuestionId = (0, qandas_1.getLatestAnsweredQuestion)(updatedComments);
13
+ try {
14
+ // Update the cache with the new comments
15
+ dispatch(comments_api_1.nextCommentsApi.util.updateQueryData('getQandAStream',
16
+ // Updates happen only for the live stream, so we pass the status as 'live' hardcoded
17
+ { storyId, useStaging, commentsAPIUrl, status: 'live' }, (draft) => {
18
+ // NB - if the same comment is updated more than once, the most recent thing added to the array will be the one that is shown
19
+ for (let c = 0; c < updatedComments.length; c++) {
20
+ const commentId = updatedComments[c].id;
21
+ const commentIndex = draft.qandas.findIndex((d) => d.id === commentId);
22
+ if (commentIndex > -1) {
23
+ draft.qandas[commentIndex] = updatedComments[c];
24
+ }
25
+ else {
26
+ draft.qandas.unshift(updatedComments[c]);
27
+ }
28
+ }
29
+ }));
30
+ // Clear out the updated comments
31
+ dispatch(comments_api_1.nextCommentsApi.util.updateQueryData('getQandAUpdates', { storyId, useStaging, commentsAPIUrl }, (draft) => {
32
+ draft.updatedComments = [];
33
+ }));
34
+ dispatch((0, stream_1.setLatestAnsweredQuestionId)(latestAnsweredQuestionId));
35
+ }
36
+ catch (error) {
37
+ console.log('error', error);
38
+ }
39
+ };
40
+ exports.updateComments = updateComments;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ // this will get the user token and display name from oComments library and save it to the redux store
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.user = exports.getUserProfile = void 0;
8
+ const toolkit_1 = require("@reduxjs/toolkit");
9
+ const auth_1 = __importDefault(require("../utils/auth"));
10
+ const ACTION_ROOT = 'user';
11
+ const ACTION_GET_USER_PROFILE = `${ACTION_ROOT}/profile`;
12
+ const initialState = {
13
+ profile: {
14
+ token: '',
15
+ displayName: '',
16
+ isRegistered: false,
17
+ isTrialist: false,
18
+ isSubscribed: false,
19
+ },
20
+ loading: false,
21
+ error: '',
22
+ };
23
+ exports.getUserProfile = (0, toolkit_1.createAsyncThunk)(ACTION_GET_USER_PROFILE, async ({ useStaging, displayName = '', commentsAPIUrl }) => {
24
+ const userDetails = await (0, auth_1.default)({
25
+ useStaging,
26
+ displayName,
27
+ commentsAPIUrl,
28
+ });
29
+ return userDetails;
30
+ });
31
+ exports.user = (0, toolkit_1.createSlice)({
32
+ name: 'user',
33
+ initialState,
34
+ reducers: {}, // No local reducers in this case
35
+ extraReducers: (builder) => {
36
+ builder
37
+ .addCase(exports.getUserProfile.pending, (state) => {
38
+ state.loading = true;
39
+ })
40
+ .addCase(exports.getUserProfile.fulfilled, (state, action) => {
41
+ state.loading = false;
42
+ state.profile = action.payload;
43
+ state.error = '';
44
+ })
45
+ .addCase(exports.getUserProfile.rejected, (state) => {
46
+ state.loading = false;
47
+ state.error =
48
+ 'We could not retrieve your profile. Please try again later.';
49
+ });
50
+ },
51
+ });
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const o_comments_1 = require("@financial-times/o-comments");
4
+ const PROD_AUTH_URL = 'https://comments-api.ft.com';
5
+ const TEST_AUTH_URL = 'https://comments-api-test.ft.com';
6
+ exports.default = async ({ useStaging = true, displayName = '', commentsAPIUrl = PROD_AUTH_URL, }) => {
7
+ const userDetails = await o_comments_1.auth.fetchJsonWebToken({
8
+ useStagingEnvironment: useStaging,
9
+ onlySubscribers: true,
10
+ displayName,
11
+ commentsAPIUrl: commentsAPIUrl === PROD_AUTH_URL ? PROD_AUTH_URL : TEST_AUTH_URL,
12
+ });
13
+ // if the JWT creation has failed, a `userHasValidSession` value will be returned
14
+ // our application assumes the values of a JWT are saved to state, so we set the
15
+ // default values
16
+ if (userDetails.userHasValidSession) {
17
+ return {
18
+ token: '',
19
+ displayName: '',
20
+ isSubscribed: userDetails.userHasValidSession,
21
+ };
22
+ }
23
+ else {
24
+ return userDetails;
25
+ }
26
+ };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLatestAnsweredQuestion = void 0;
4
+ // Returns the question UUID with the latest answer
5
+ // eslint-disable-next-line import/prefer-default-export
6
+ const getLatestAnsweredQuestion = (questions) => {
7
+ let questionId = '';
8
+ let latestTimestamp = 0;
9
+ questions.forEach((question) => {
10
+ question.children?.forEach((answer) => {
11
+ const answerTimestamp = new Date(answer.publishedDate).getTime();
12
+ if (answerTimestamp > latestTimestamp) {
13
+ questionId = question.id;
14
+ latestTimestamp = answerTimestamp;
15
+ }
16
+ });
17
+ });
18
+ return questionId;
19
+ };
20
+ exports.getLatestAnsweredQuestion = getLatestAnsweredQuestion;
@@ -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,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = validateDisplayName;
4
+ function validateDisplayName(displayName) {
5
+ /*
6
+ * Allowed Characters
7
+ * The below regex matches any character not in the allowed characters set
8
+ * All allowed characters are case insensitive
9
+ * Anything in the range A-Z or 0-9 is allowed
10
+ * Any of the below special characters are allowed
11
+ * !#$%'`()*+,-./:;=@[]^_{|}
12
+ * Spaces are allowed
13
+ */
14
+ const matchInvalidCharacters = /[^0-9a-z!#$%'`()*+,\-.\/:;=@[\]\^_{}\|\s]/gi;
15
+ const matchingCharacters = displayName.match(matchInvalidCharacters);
16
+ const uniqueMatchingCharacters = matchingCharacters && matchingCharacters.length
17
+ ? matchingCharacters.filter((character, position) => matchingCharacters.indexOf(character) === position)
18
+ : [];
19
+ return uniqueMatchingCharacters.length
20
+ ? uniqueMatchingCharacters.join('')
21
+ : false;
22
+ }
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import Headshot from '@financial-times/cp-content-pipeline-ui/lib/components/Headshot';
3
+ import Byline from '@financial-times/cp-content-pipeline-ui/lib/components/Byline';
4
+ function Author({ byline, headshot, prefLabel, role, streamPage, }) {
5
+ const hasHeadshot = headshot && prefLabel;
6
+ return (_jsxs("div", { className: "qanda-block__answer__author", "data-testid": "qanda-block__answer__author", children: [hasHeadshot ? (_jsx("div", { className: "qanda-block__answer__author-headshot", children: _jsx(Headshot, { headshot: headshot, prefLabel: prefLabel, streamPage: streamPage, className: "qanda-block__answer__author-headshot" }) })) : (_jsx("div", { className: "qanda-block__answer__author-opinion-icon" })), _jsxs("div", { className: "qanda-block__answer__author-info", children: [_jsx("div", { className: "o3-type-body-highlight qanda-block__answer__author-name", children: _jsx(Byline, { structuredContent: byline, showEditedBy: false }) }), _jsx("div", { className: "o3-type-body-base", children: role })] })] }));
7
+ }
8
+ Author.defaultProps = {
9
+ byline: '',
10
+ headshot: false,
11
+ prefLabel: false,
12
+ streamPage: '',
13
+ };
14
+ export default Author;