@financial-times/qanda-ui 0.0.1-beta.3 → 0.0.1-beta.31

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 (129) hide show
  1. package/README.md +39 -7
  2. package/dist/cjs/components/Author/index.js +19 -0
  3. package/dist/cjs/components/Calendar/Apps.js +40 -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 +48 -0
  7. package/dist/cjs/components/CountdownTimer/index.js +91 -0
  8. package/dist/cjs/components/EmbedFormField/index.js +25 -0
  9. package/dist/cjs/components/ErrorMessage/humanReadableError.js +74 -0
  10. package/dist/cjs/components/ErrorMessage/index.js +60 -0
  11. package/dist/cjs/components/Expander/index.js +72 -0
  12. package/dist/cjs/components/ExpertDrawer/index.js +37 -0
  13. package/dist/cjs/components/FloatingActionBar/index.js +62 -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 +15 -0
  17. package/dist/cjs/components/Qanda.js +37 -0
  18. package/dist/cjs/components/QandaBlock/index.js +79 -0
  19. package/dist/cjs/components/QandaContainer.js +92 -0
  20. package/dist/cjs/components/QandaProvider.js +11 -0
  21. package/dist/cjs/components/QuestionForm/index.js +183 -0
  22. package/dist/cjs/components/QuestionForm/messagesByUserType.js +35 -0
  23. package/dist/cjs/components/Stream/index.js +37 -0
  24. package/dist/cjs/components/Survey/index.js +8 -0
  25. package/dist/cjs/config/qanda.js +6 -0
  26. package/dist/cjs/hooks/useIsMobile.js +15 -0
  27. package/dist/cjs/index.js +53 -0
  28. package/dist/cjs/services/comments-api.js +187 -0
  29. package/dist/cjs/services/error-middleware.js +47 -0
  30. package/dist/cjs/services/listeners-middleware.js +121 -0
  31. package/dist/cjs/services/reliability.js +79 -0
  32. package/dist/cjs/store/expertSlice.js +26 -0
  33. package/dist/cjs/store/index.js +32 -0
  34. package/dist/cjs/store/questionSlice.js +32 -0
  35. package/dist/cjs/store/stream.js +35 -0
  36. package/dist/cjs/store/updateComments.js +40 -0
  37. package/dist/cjs/store/user.js +51 -0
  38. package/dist/cjs/utils/auth.js +26 -0
  39. package/dist/cjs/utils/qandas.js +20 -0
  40. package/dist/cjs/utils/strings.js +16 -0
  41. package/dist/cjs/utils/tracking.js +34 -0
  42. package/dist/cjs/utils/validate-display-name.js +22 -0
  43. package/dist/esm/components/Author/index.js +14 -0
  44. package/dist/esm/components/Calendar/Apps.js +38 -0
  45. package/dist/esm/components/Calendar/CalendarList.js +7 -0
  46. package/dist/esm/components/Calendar/CalendarListItem.js +22 -0
  47. package/dist/esm/components/Calendar/index.js +43 -0
  48. package/dist/esm/components/CountdownTimer/index.js +88 -0
  49. package/dist/esm/components/EmbedFormField/index.js +23 -0
  50. package/dist/esm/components/ErrorMessage/humanReadableError.js +71 -0
  51. package/dist/esm/components/ErrorMessage/index.js +54 -0
  52. package/dist/esm/components/Expander/index.js +70 -0
  53. package/dist/esm/components/ExpertDrawer/index.js +32 -0
  54. package/dist/esm/components/FloatingActionBar/index.js +60 -0
  55. package/dist/esm/components/Loader/index.js +4 -0
  56. package/dist/esm/components/MessageBox/index.js +5 -0
  57. package/dist/esm/components/Overlay/index.js +10 -0
  58. package/dist/esm/components/Qanda.js +32 -0
  59. package/dist/esm/components/QandaBlock/index.js +74 -0
  60. package/dist/esm/components/QandaContainer.js +87 -0
  61. package/dist/esm/components/QandaProvider.js +8 -0
  62. package/dist/esm/components/QuestionForm/index.js +177 -0
  63. package/dist/esm/components/QuestionForm/messagesByUserType.js +30 -0
  64. package/dist/esm/components/Stream/index.js +32 -0
  65. package/dist/esm/components/Survey/index.js +6 -0
  66. package/dist/esm/config/qanda.js +3 -0
  67. package/dist/esm/hooks/useIsMobile.js +12 -0
  68. package/dist/esm/index.js +46 -0
  69. package/dist/esm/services/comments-api.js +182 -0
  70. package/dist/esm/services/error-middleware.js +43 -0
  71. package/dist/esm/services/listeners-middleware.js +118 -0
  72. package/dist/esm/services/reliability.js +75 -0
  73. package/dist/esm/store/expertSlice.js +22 -0
  74. package/dist/esm/store/index.js +26 -0
  75. package/dist/esm/store/questionSlice.js +28 -0
  76. package/dist/esm/store/stream.js +31 -0
  77. package/dist/esm/store/updateComments.js +36 -0
  78. package/dist/esm/store/user.js +45 -0
  79. package/dist/esm/utils/auth.js +24 -0
  80. package/dist/esm/utils/qandas.js +16 -0
  81. package/dist/esm/utils/strings.js +13 -0
  82. package/dist/esm/utils/tracking.js +29 -0
  83. package/dist/esm/utils/validate-display-name.js +19 -0
  84. package/dist/qanda.scss +841 -0
  85. package/dist/types/components/Author/index.d.ts +18 -0
  86. package/dist/types/components/Calendar/Apps.d.ts +14 -0
  87. package/dist/types/components/Calendar/CalendarList.d.ts +2 -0
  88. package/dist/types/components/Calendar/CalendarListItem.d.ts +8 -0
  89. package/dist/types/components/Calendar/index.d.ts +2 -0
  90. package/dist/types/components/CountdownTimer/index.d.ts +6 -0
  91. package/dist/types/components/EmbedFormField/index.d.ts +5 -0
  92. package/dist/types/components/ErrorMessage/humanReadableError.d.ts +29 -0
  93. package/dist/types/components/ErrorMessage/index.d.ts +8 -0
  94. package/dist/types/components/Expander/index.d.ts +8 -0
  95. package/dist/types/components/ExpertDrawer/index.d.ts +2 -0
  96. package/dist/types/components/FloatingActionBar/index.d.ts +4 -0
  97. package/dist/types/components/Loader/index.d.ts +14 -0
  98. package/dist/types/components/MessageBox/index.d.ts +10 -0
  99. package/dist/types/components/Overlay/index.d.ts +12 -0
  100. package/dist/types/components/Qanda.d.ts +3 -0
  101. package/dist/types/components/QandaBlock/index.d.ts +7 -0
  102. package/dist/types/components/QandaContainer.d.ts +6 -0
  103. package/dist/types/components/QandaProvider.d.ts +9 -0
  104. package/dist/types/components/QuestionForm/index.d.ts +1 -0
  105. package/dist/types/components/QuestionForm/messagesByUserType.d.ts +7 -0
  106. package/dist/types/components/Stream/index.d.ts +5 -0
  107. package/dist/types/components/Survey/index.d.ts +7 -0
  108. package/dist/types/config/qanda.d.ts +1 -0
  109. package/dist/types/hooks/useIsMobile.d.ts +1 -0
  110. package/dist/types/index.d.ts +17 -0
  111. package/dist/types/services/comments-api.d.ts +64 -0
  112. package/dist/types/services/error-middleware.d.ts +2 -0
  113. package/dist/types/services/listeners-middleware.d.ts +11 -0
  114. package/dist/types/services/reliability.d.ts +19 -0
  115. package/dist/types/store/expertSlice.d.ts +14 -0
  116. package/dist/types/store/index.d.ts +258 -0
  117. package/dist/types/store/questionSlice.d.ts +16 -0
  118. package/dist/types/store/stream.d.ts +18 -0
  119. package/dist/types/store/updateComments.d.ts +3 -0
  120. package/dist/types/store/user.d.ts +26 -0
  121. package/dist/types/utils/auth.d.ts +6 -0
  122. package/dist/types/utils/qandas.d.ts +2 -0
  123. package/dist/types/utils/strings.d.ts +1 -0
  124. package/dist/types/utils/tracking.d.ts +4 -0
  125. package/dist/types/utils/validate-display-name.d.ts +1 -0
  126. package/package.json +36 -30
  127. package/dist/index.html +0 -21
  128. package/dist/index.js +0 -2
  129. package/dist/index.js.LICENSE.txt +0 -15
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # QandA-UI
2
2
 
3
- A library for the Live Q&A client-side component
3
+ A library for the Live Q&A (AKA Ask an Expert) client-side component
4
4
 
5
5
  ## Usage
6
6
 
@@ -20,9 +20,13 @@ The Origami components required are listed as peer dependencies in package.json.
20
20
 
21
21
  This library exports a class which can be used to construct a new instance of QandaUI and exposes methods to initialise it and to destroy it.
22
22
 
23
+ It also exports QandaBlock, the UI that is used to show a question 'block' - a question and its answers along with the other data like poster name and expert details
24
+
25
+ #### QandaUI
26
+
23
27
  You can pass the necessary settings to Qanda UI in either the JS or in the container element.
24
28
 
25
- #### Example:
29
+ ##### Example:
26
30
 
27
31
  ```jsx
28
32
  // JSX
@@ -44,7 +48,7 @@ const qandaUI = new QandaUI(qandaEl, {
44
48
  qandaUI.init();
45
49
  ```
46
50
 
47
- #### Alternative:
51
+ ##### Alternative:
48
52
 
49
53
  ```jsx
50
54
  // JSX
@@ -66,7 +70,7 @@ const qandaUI = new QandaUI(qandaEl);
66
70
  qandaUI.init();
67
71
  ```
68
72
 
69
- #### Parameters
73
+ ##### Parameters
70
74
 
71
75
  - storyId: (required) string, the ID of the article used for Live Q&A
72
76
  - useStaging: (optional) boolean, set to true if you want to use the staging Coral
@@ -75,11 +79,25 @@ qandaUI.init();
75
79
  - commentsAPIVersion: (required) string, the version of the API to be used
76
80
  - maxQuestionLength: (optional) number, set to the maximum allowable length (in characters) for the question text
77
81
 
78
- #### Instance Methods
82
+ ##### Instance Methods
79
83
 
80
84
  - `myInstance.init()` - render the Qanda UI
81
85
  - `myInstance.destroy()` - remove the Qanda instance
82
86
 
87
+ ### QandaBlock
88
+
89
+ ```jsx
90
+ import QandaBlock from '@financial-times/qanda-ui';
91
+
92
+ <QandaBlock
93
+ id={questionId}
94
+ body={body}
95
+ children={answers}
96
+ byline={byeline}
97
+ summary={lorem}
98
+ />;
99
+ ```
100
+
83
101
  ## Development
84
102
 
85
103
  ### Prerequisites
@@ -111,8 +129,8 @@ You can customize the demo application by modifying the configuration files in t
111
129
 
112
130
  These are the config settings you can work with:
113
131
 
132
+ - `export const COMMENTS_API_BASE_HOST = LOCAL_API;` // use one of the provided constants or set your own next comments api domain
114
133
  - `export const USE_STAGING = true;` // use Coral staging or production
115
- - `export const USE_LOCAL_API = true;` // use local next-comments-api or live
116
134
 
117
135
  ### Building and Running the Production Server
118
136
 
@@ -132,7 +150,7 @@ To run the Jest tests, use:
132
150
  npm run test
133
151
  ```
134
152
 
135
- ## Technology
153
+ ### Technology
136
154
 
137
155
  This uses React, Redux and RTK Query to create the components and manage their states.
138
156
 
@@ -140,6 +158,20 @@ This uses React, Redux and RTK Query to create the components and manage their s
140
158
  - The store is used for non-API data that needs to be shared around the components
141
159
  - The story ID and useStaging settings passed in when the Qanda compontent is set are saved into the Context.
142
160
 
161
+ ### Architecture
162
+
163
+ Quanda UI is strongly coupled with next-comments-api. See [architectural documentation in next-comments-api repository](https://github.com/Financial-Times/next-comments-api/blob/main/docs/liveqa.md#live-qa).
164
+
165
+ ## Support
166
+
167
+ ### Client-side metrics
168
+
169
+ We send client-side metrics (Failures and optionally successes) to [Cloudwatch RUM in the `FT Tech Next Prod` account](https://eu-west-1.console.aws.amazon.com/cloudwatch/home?region=eu-west-1#rum:dashboard/cp-client-metrics?tab=events).
170
+
171
+ Failure events may contain error information for debugging.
172
+
173
+ Those events are also observable in a [Live Q&A Grafana dashboard](https://grafana.ft.com/d/fekh6pkkr80lcd/live-qanda).
174
+
143
175
  ### How updates work
144
176
 
145
177
  In order to receive updates from the next comments API, we use an EventSource that listens to an EventEmitter endpoint on next-comments-api.
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const Headshot_1 = __importDefault(require("@financial-times/cp-content-pipeline-ui/lib/components/Headshot"));
8
+ const Byline_1 = __importDefault(require("@financial-times/cp-content-pipeline-ui/lib/components/Byline"));
9
+ function Author({ byline, headshot, prefLabel, role, streamPage, }) {
10
+ const hasHeadshot = headshot && prefLabel;
11
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__answer__author", "data-testid": "qanda-block__answer__author", children: [hasHeadshot ? ((0, jsx_runtime_1.jsx)("div", { className: "qanda-block__answer__author-headshot", children: (0, jsx_runtime_1.jsx)(Headshot_1.default, { headshot: headshot, prefLabel: prefLabel, streamPage: streamPage, className: "qanda-block__answer__author-headshot" }) })) : ((0, jsx_runtime_1.jsx)("div", { className: "qanda-block__answer__author-opinion-icon" })), (0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__answer__author-info", children: [(0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-highlight qanda-block__answer__author-name", children: (0, jsx_runtime_1.jsx)(Byline_1.default, { structuredContent: byline, showEditedBy: false }) }), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-base", children: role })] })] }));
12
+ }
13
+ Author.defaultProps = {
14
+ byline: '',
15
+ headshot: false,
16
+ prefLabel: false,
17
+ streamPage: '',
18
+ };
19
+ exports.default = Author;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("preact/jsx-runtime");
4
+ const qanda_1 = require("../../config/qanda");
5
+ const tracking_1 = require("../../utils/tracking");
6
+ function formatDate(date) {
7
+ return date.replace(/-|:|\.\d+/g, '');
8
+ }
9
+ const CalendarLink = ({ href, children, }) => {
10
+ return ((0, jsx_runtime_1.jsx)("a", { href: href, target: "_blank", rel: "noopener noreferrer", role: "menuitem", "data-trackable": (0, tracking_1.trackingNS)('calendar-link'), className: "live-qa-calendar-list__item__link", children: children }));
11
+ };
12
+ function itemChildren(name, icon) {
13
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "live-qa-calendar-list__item__icon", children: icon }), (0, jsx_runtime_1.jsx)("span", { className: "live-qa-calendar-list__item__text", children: name })] }));
14
+ }
15
+ const description = (url) => `To join the ${qanda_1.PRODUCT_NAME} session, visit: ${url}`;
16
+ const apps = [
17
+ {
18
+ name: 'Gmail',
19
+ icon: ((0, jsx_runtime_1.jsx)("svg", { className: "live-qa-calendar-icon", xmlns: "http://www.w3.org/2000/svg", width: "24", height: "25", viewBox: "0 0 24 25", fill: "none", children: (0, jsx_runtime_1.jsxs)("g", { transform: "translate(4, 3.36)", children: [(0, jsx_runtime_1.jsx)("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M15.8514 7.64502H8.16309V10.8162H12.5564C12.3672 11.841 11.792 12.7093 10.9275 13.2906V15.3476H13.5657C15.1093 13.9204 15.9998 11.8187 15.9998 9.32193C15.9998 8.7406 15.9479 8.18163 15.8514 7.64502Z", fill: "black" }), (0, jsx_runtime_1.jsx)("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M8.16315 17.3339C10.3672 17.3339 12.2151 16.5998 13.5658 15.3477L10.9275 13.2907C10.1966 13.7826 9.26149 14.0733 8.16315 14.0733C6.037 14.0733 4.23737 12.6311 3.59544 10.6934H0.868164V12.8174C2.21139 15.4968 4.97206 17.3339 8.16315 17.3339Z", fill: "black" }), (0, jsx_runtime_1.jsx)("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M3.59555 10.6933C3.43228 10.2014 3.33952 9.67595 3.33952 9.13561C3.33952 8.59527 3.43228 8.06984 3.59555 7.57795V5.45386H0.868275C0.315399 6.56062 0 7.81272 0 9.13561C0 10.4585 0.315399 11.7106 0.868275 12.8174L3.59555 10.6933Z", fill: "black" }), (0, jsx_runtime_1.jsx)("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M8.16316 4.19816C9.36167 4.19816 10.4377 4.6118 11.2837 5.42417L13.6251 3.07277C12.2114 1.74987 10.3635 0.9375 8.16316 0.9375C4.97206 0.9375 2.21139 2.77465 0.868164 5.45399L3.59544 7.57808C4.23737 5.64031 6.037 4.19816 8.16316 4.19816Z", fill: "black" })] }) })),
20
+ getLink: ({ title, startDateTime, endDateTime, url }) => `https://www.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(title)}&dates=${formatDate(startDateTime)}/${formatDate(endDateTime)}&details=${encodeURIComponent(description(url))}&location=${encodeURIComponent(url)}`,
21
+ },
22
+ {
23
+ name: 'Outlook',
24
+ icon: ((0, jsx_runtime_1.jsx)("svg", { className: "live-qa-calendar-icon", xmlns: "http://www.w3.org/2000/svg", width: "24", height: "25", viewBox: "0 0 24 25", fill: "none", children: (0, jsx_runtime_1.jsx)("path", { d: "M21.9979 8.49306V17.3819C22.0068 17.5304 21.987 17.6792 21.9399 17.8185C21.8927 17.9578 21.8194 18.0842 21.7247 18.1894C21.63 18.2946 21.5162 18.3761 21.3909 18.4285C21.2656 18.4809 21.1316 18.5029 20.9981 18.4931H13.9988V22.9375L2 20.9375V4.9375L13.9988 2.9375V7.38194H20.9981C21.1316 7.37212 21.2656 7.39413 21.3909 7.4465C21.5162 7.49886 21.63 7.58037 21.7247 7.68558C21.8194 7.79078 21.8927 7.91724 21.9399 8.0565C21.987 8.19576 22.0068 8.3446 21.9979 8.49306ZM20.9981 8.60417L16.9985 11.1597L13.9988 9.27083V11.0486L16.9985 12.9375L20.9981 10.3819V8.60417ZM7.91939 8.60417C5.81961 8.52639 4.78971 10.5819 4.78971 13.0486C4.78971 15.5153 5.8596 17.4931 7.91939 17.4931C9.7892 17.4931 10.9991 15.6153 10.9991 13.1708C10.9991 10.7264 10.1292 8.72639 7.91939 8.6375M7.60943 10.3819C8.44934 10.3819 9.19926 11.2042 9.19926 12.8819C9.19926 14.5597 8.44934 15.5042 7.60943 15.5042C6.76951 15.5042 6.09958 14.2708 6.09958 12.9375C6.09958 11.6042 6.77951 10.3819 7.60943 10.3819Z", fill: "black" }) })),
25
+ getLink: ({ title, startDateTime, endDateTime, url }) => `https://outlook.live.com/calendar/0/deeplink/compose?subject=${encodeURIComponent(title)}&startdt=${startDateTime}&enddt=${endDateTime}&body=${encodeURIComponent(description(url))}&location=${encodeURIComponent(url)}`,
26
+ },
27
+ {
28
+ name: 'Yahoo',
29
+ icon: ((0, jsx_runtime_1.jsx)("svg", { className: "live-qa-calendar-icon", xmlns: "http://www.w3.org/2000/svg", width: "24", height: "25", viewBox: "0 0 24 25", fill: "none", children: (0, jsx_runtime_1.jsx)("path", { d: "M15.3929 13.7528C15.3614 13.7513 15.3247 13.7506 15.2879 13.7506C13.8674 13.7506 12.7109 14.8793 12.6659 16.2886V16.2931C12.7049 17.6536 13.8172 18.7411 15.1837 18.7411C15.2182 18.7411 15.2519 18.7403 15.2857 18.7388H15.2812C15.3194 18.7411 15.3637 18.7418 15.4087 18.7418C16.8217 18.7418 17.9737 17.6228 18.0269 16.2226V16.2181C18.0037 14.8501 16.8892 13.7498 15.5174 13.7498C15.4739 13.7498 15.4304 13.7513 15.3869 13.7528H15.3929ZM0.75293 7.97933L5.57993 18.8138L3.84593 22.7221H8.09168L14.5214 7.97933H10.3042L7.67993 14.2868L5.08343 7.97933H0.75368H0.75293ZM18.4297 3.15308L14.1277 12.8161H18.9359L23.2469 3.15308H18.4297Z", fill: "black" }) })),
30
+ getLink: ({ title, startDateTime, endDateTime, url }) => `https://calendar.yahoo.com/?v=60&title=${encodeURIComponent(title)}&st=${formatDate(startDateTime)}&et=${formatDate(endDateTime)}&desc=${encodeURIComponent(description(url))}&in_loc=${encodeURIComponent(url)}`,
31
+ },
32
+ ];
33
+ exports.default = apps.map(({ name, icon, getLink }) => ({
34
+ name,
35
+ icon,
36
+ link: ({ title, startDateTime, endDateTime, url, }) => {
37
+ const href = getLink({ title, startDateTime, endDateTime, url });
38
+ return (0, jsx_runtime_1.jsx)(CalendarLink, { href: href, children: itemChildren(name, icon) });
39
+ },
40
+ }));
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const CalendarListItem_1 = __importDefault(require("./CalendarListItem"));
8
+ const Apps_1 = __importDefault(require("./Apps"));
9
+ function CalendarList() {
10
+ return ((0, jsx_runtime_1.jsx)("ul", { className: "live-qa-calendar-list", id: "live-qa-calendar-list-menu", "data-testid": "live-qa-calendar-list-menu", role: "menu", "aria-labelledby": "live-qa-calendar-button-menu", children: Apps_1.default && Apps_1.default.map((app) => (0, jsx_runtime_1.jsx)(CalendarListItem_1.default, { ...app })) }));
11
+ }
12
+ exports.default = CalendarList;
@@ -0,0 +1,24 @@
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 QandaProvider_1 = require("../QandaProvider");
6
+ const comments_api_1 = require("../../services/comments-api");
7
+ function CalendarListItem({ name, link }) {
8
+ const { storyId, useStaging, commentsAPIUrl, title } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
9
+ const { data } = (0, comments_api_1.useGetQandAStreamQuery)({
10
+ storyId,
11
+ useStaging,
12
+ commentsAPIUrl,
13
+ // Calendar is only relevant for pre-live Q&A
14
+ status: 'pre-live',
15
+ });
16
+ return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: data?.startDateTime && data?.endDateTime && ((0, jsx_runtime_1.jsx)("li", { className: 'live-qa-calendar-list__item', role: "presentation", "data-testid": "live-qa-calendar-list-item", children: link({
17
+ title,
18
+ startDateTime: data.startDateTime,
19
+ endDateTime: data.endDateTime,
20
+ url: `https://www.ft.com/content/${storyId}`,
21
+ children: name,
22
+ }) })) }));
23
+ }
24
+ exports.default = CalendarListItem;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const o3_button_1 = require("@financial-times/o3-button");
8
+ const react_1 = require("react");
9
+ const CalendarList_1 = __importDefault(require("./CalendarList"));
10
+ const comments_api_1 = require("../../services/comments-api");
11
+ const QandaProvider_1 = require("../QandaProvider");
12
+ const tracking_1 = require("../../utils/tracking");
13
+ function Calendar() {
14
+ const [dropDownVisible, setDropDownVisible] = (0, react_1.useState)(false);
15
+ const containerRef = (0, react_1.useRef)(null);
16
+ const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
17
+ const { data } = (0, comments_api_1.useGetQandAStreamQuery)({
18
+ storyId,
19
+ useStaging,
20
+ commentsAPIUrl,
21
+ // Calendar is only relevant for pre-live Q&A
22
+ // so we can hardcode the status here
23
+ status: 'pre-live',
24
+ });
25
+ (0, react_1.useEffect)(() => {
26
+ const handleClickOutside = (event) => {
27
+ if (containerRef.current &&
28
+ event.target instanceof Node &&
29
+ !containerRef.current.contains(event.target)) {
30
+ setDropDownVisible(false);
31
+ }
32
+ };
33
+ document.addEventListener('click', handleClickOutside);
34
+ return () => {
35
+ document.removeEventListener('click', handleClickOutside);
36
+ };
37
+ }, []);
38
+ return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: data?.status === 'pre-live' && ((0, jsx_runtime_1.jsxs)("div", { className: "live-qa-calendar-container", ref: containerRef, children: [(0, jsx_runtime_1.jsx)(o3_button_1.Button, { label: "Add to your calendar", type: "secondary", theme: "mono", icon: "calendar", iconOnly: false, attributes: {
39
+ className: 'live-qa-calendar-button',
40
+ 'aria-pressed': dropDownVisible,
41
+ 'aria-haspopup': 'true',
42
+ 'aria-controls': 'menu',
43
+ id: 'live-qa-calendar-button-menu',
44
+ 'data-testid': 'live-qa-calendar-button',
45
+ 'data-trackable': (0, tracking_1.trackingNS)('add-to-calendar-button'),
46
+ }, onClick: () => setDropDownVisible(!dropDownVisible) }), dropDownVisible && (0, jsx_runtime_1.jsx)(CalendarList_1.default, {})] })) }));
47
+ }
48
+ exports.default = Calendar;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatTime = formatTime;
4
+ const jsx_runtime_1 = require("preact/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const QandaProvider_1 = require("../QandaProvider");
7
+ const comments_api_1 = require("../../services/comments-api");
8
+ function formatTime(remainingTime) {
9
+ const days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
10
+ if (days >= 1) {
11
+ return `${days} day${days === 1 ? '' : 's'} `;
12
+ }
13
+ const hours = Math.floor((remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
14
+ if (hours >= 1) {
15
+ return `${hours} hour${hours === 1 ? '' : 's'}`;
16
+ }
17
+ const minutes = Math.floor((remainingTime % (1000 * 60 * 60)) / (1000 * 60));
18
+ if (minutes >= 1) {
19
+ return `${minutes} minute${minutes === 1 ? '' : 's'}`;
20
+ }
21
+ const seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);
22
+ if (seconds >= 1) {
23
+ return `${seconds} second${seconds === 1 ? '' : 's'}`;
24
+ }
25
+ }
26
+ function CountdownTimer({ container }) {
27
+ const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
28
+ const { data } = (0, comments_api_1.useGetQandAStreamQuery)({
29
+ storyId,
30
+ useStaging,
31
+ commentsAPIUrl,
32
+ // Countdown is only relevant for pre-live Q&A
33
+ // so we can hardcode the status here
34
+ status: 'pre-live',
35
+ });
36
+ const updatedQA = (0, comments_api_1.useUpdatedQA)({
37
+ storyId,
38
+ useStaging,
39
+ commentsAPIUrl,
40
+ });
41
+ const [count, setCount] = (0, react_1.useState)('');
42
+ const status = updatedQA?.status || data?.status || 'pre-live';
43
+ const startDateTime = updatedQA?.startDateTime || data?.startDateTime;
44
+ const endDateTime = updatedQA?.endDateTime || data?.endDateTime;
45
+ (0, react_1.useEffect)(() => {
46
+ function calculateTimeLeft() {
47
+ const currentDate = Date.now();
48
+ const isLive = status === 'live';
49
+ const timeForCountdown = isLive ? endDateTime : startDateTime;
50
+ if (!timeForCountdown) {
51
+ return;
52
+ }
53
+ const remainingTime = new Date(timeForCountdown).getTime() - currentDate;
54
+ const eventInfo = isLive ? 'Ending' : 'Starting';
55
+ if (remainingTime !== null && !isNaN(remainingTime)) {
56
+ if (remainingTime >= 1000) {
57
+ setCount(`${eventInfo} in ${formatTime(remainingTime)}`);
58
+ }
59
+ else {
60
+ setCount(`${eventInfo} Soon`);
61
+ }
62
+ }
63
+ }
64
+ calculateTimeLeft();
65
+ const interval = setInterval(calculateTimeLeft, 1000);
66
+ return () => clearInterval(interval);
67
+ }, [
68
+ data?.status,
69
+ data?.startDateTime,
70
+ data?.endDateTime,
71
+ updatedQA?.status,
72
+ updatedQA?.startDateTime,
73
+ updatedQA?.endDateTime,
74
+ ]);
75
+ const [portalTarget, setPortalTarget] = (0, react_1.useState)();
76
+ (0, react_1.useEffect)(() => {
77
+ if (container) {
78
+ const portalContainer = document.createElement('div');
79
+ container.prepend(portalContainer);
80
+ setPortalTarget(portalContainer);
81
+ return () => {
82
+ portalContainer.remove();
83
+ };
84
+ }
85
+ }, []);
86
+ const portalElement = data &&
87
+ ((status === 'pre-live' && startDateTime) ||
88
+ (status === 'live' && endDateTime)) && ((0, jsx_runtime_1.jsx)("div", { className: "qanda-portal countdown-wrapper", children: (0, jsx_runtime_1.jsx)("div", { className: "countdown-block", children: (0, jsx_runtime_1.jsx)("p", { className: "countdown", children: count && ((0, jsx_runtime_1.jsx)("time", { datetime: status === 'pre-live' ? startDateTime : endDateTime, children: count })) }) }) }));
89
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, react_1.createPortal)(portalElement, portalTarget || document.body) });
90
+ }
91
+ exports.default = CountdownTimer;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("preact/jsx-runtime");
4
+ const react_redux_1 = require("react-redux");
5
+ const questionSlice_1 = require("../../store/questionSlice");
6
+ const react_1 = require("react");
7
+ const react_dom_1 = require("react-dom");
8
+ const qanda_1 = require("../../config/qanda");
9
+ function EmbedFormField({ container }) {
10
+ const dispatch = (0, react_redux_1.useDispatch)();
11
+ const [portalTarget, setPortalTarget] = (0, react_1.useState)();
12
+ (0, react_1.useEffect)(() => {
13
+ if (container) {
14
+ const portalContainer = document.createElement('div');
15
+ container.prepend(portalContainer);
16
+ setPortalTarget(portalContainer);
17
+ return () => {
18
+ portalContainer.remove();
19
+ };
20
+ }
21
+ }, []);
22
+ const portalElement = ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-portal embed-form-field__container o3-type-body-highlight", children: ["Submit your question for ", qanda_1.PRODUCT_NAME, (0, jsx_runtime_1.jsxs)("div", { onClick: () => dispatch((0, questionSlice_1.openForm)()), className: "embed-form-field__text-input o3-type-body-lg", "data-testid": "embed-form-field", children: [(0, jsx_runtime_1.jsx)("span", { className: "embed-form-field__icon", children: (0, jsx_runtime_1.jsx)("i", { style: { maskImage: 'var(--o3-icon-user)' } }) }), "Enter your question"] })] }));
23
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, react_dom_1.createPortal)(portalElement, portalTarget || document.body) });
24
+ }
25
+ exports.default = EmbedFormField;
@@ -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,60 @@
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
+ content: {
46
+ uuid: storyId,
47
+ },
48
+ trigger_action: operation,
49
+ custom: [
50
+ {
51
+ name: 'state',
52
+ value: 'error',
53
+ },
54
+ ...errorDetails,
55
+ ],
56
+ });
57
+ // Ensure trackEvent is only called once
58
+ }, []);
59
+ 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 }));
60
+ }
@@ -0,0 +1,72 @@
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 o3_button_1 = require("@financial-times/o3-button");
6
+ const tracking_1 = require("../../utils/tracking");
7
+ // Generic debounce function
8
+ function debounce(func, delay) {
9
+ let timerId;
10
+ return (...args) => {
11
+ if (timerId)
12
+ clearTimeout(timerId);
13
+ timerId = setTimeout(() => {
14
+ func(...args);
15
+ }, delay);
16
+ };
17
+ }
18
+ // While comments submitted by Coral UI are always wrapped in paragraphs,
19
+ // comments submitted via our `Ask a question` form are just text.
20
+ // A wrapping paragraph is always needed for the expander to work correctly.
21
+ function EnsureParagraph({ children }) {
22
+ if (typeof children === 'object' &&
23
+ children !== null &&
24
+ 'props' in children &&
25
+ children.props?.structuredContent?.tree?.children?.[0]?.type === 'text') {
26
+ return (0, jsx_runtime_1.jsx)("p", { children: children });
27
+ }
28
+ return children;
29
+ }
30
+ function Expander({ className = '', children = [], expandLabel = 'Expand', collapseLabel = 'Collapse', id, }) {
31
+ const textRef = (0, react_1.useRef)(null);
32
+ const [requiresExpander, setExpander] = (0, react_1.useState)(false);
33
+ const [state, setState] = (0, react_1.useState)('collapsed');
34
+ const expanderContentID = `expander_content-${id}`;
35
+ (0, react_1.useEffect)(() => {
36
+ const checkNeedForExpanderButton = () => {
37
+ const element = textRef.current;
38
+ if (!element) {
39
+ return;
40
+ }
41
+ const numberOfCharacters = element.textContent?.length || 300;
42
+ const windowWidth = window.visualViewport?.width || window.innerWidth || 1200; // Fallback to desktop configuration.
43
+ // Check if the element's height exceeds 3 lines for mobile or 1 line for above mobile
44
+ // Note: This need to stay aligned with the CSS for the expander
45
+ const mobileNumberOfCharacters = 100;
46
+ const desktopNumberOfCharacters = 200;
47
+ const characterForExpander = windowWidth <= 490
48
+ ? mobileNumberOfCharacters
49
+ : desktopNumberOfCharacters;
50
+ setExpander(numberOfCharacters > characterForExpander);
51
+ };
52
+ const debouncedCheck = debounce(checkNeedForExpanderButton, 250);
53
+ // Use requestAnimationFrame to ensure layout is complete
54
+ requestAnimationFrame(checkNeedForExpanderButton);
55
+ window.addEventListener('resize', debouncedCheck);
56
+ return () => window.removeEventListener('resize', debouncedCheck);
57
+ }, [children]); // re-run whenever the children change, although we don't expect it to
58
+ const label = state === 'expanded' ? collapseLabel : expandLabel;
59
+ const expandCollapseClass = state === 'expanded' ? 'expander--expanded' : 'expander--collapsed';
60
+ const componentClassName = `${expandCollapseClass} ${className}`;
61
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `${componentClassName}`, children: [(0, jsx_runtime_1.jsx)("div", { className: `expander__text ${requiresExpander ? 'expander__text--multiline' : ''}`, id: expanderContentID, ref: textRef, children: (0, jsx_runtime_1.jsx)(EnsureParagraph, { children: children }) }), requiresExpander && ((0, jsx_runtime_1.jsx)(o3_button_1.Button, { label: label, type: "ghost", theme: "neutral", icon: state === 'expanded' ? 'chevron-up' : 'chevron-down', iconOnly: false, iconPosition: "end", attributes: {
62
+ className: 'expander__link',
63
+ 'data-trackable': (0, tracking_1.trackingNS)('expander'),
64
+ 'data-trackable-context-action': label,
65
+ 'aria-expanded': state === 'expanded' ? true : false,
66
+ 'aria-controls': expanderContentID,
67
+ 'data-action': state === 'expanded' ? 'collapse' : 'expand',
68
+ }, onClick: () => {
69
+ setState((prevState) => prevState === 'expanded' ? 'collapsed' : 'expanded');
70
+ } }))] }));
71
+ }
72
+ exports.default = Expander;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const o_comments_1 = __importDefault(require("@financial-times/o-comments"));
8
+ const react_1 = require("react");
9
+ const react_redux_1 = require("react-redux");
10
+ const expertSlice_1 = require("../../store/expertSlice");
11
+ const QandaProvider_1 = require("../QandaProvider");
12
+ const reliability_1 = require("../../services/reliability");
13
+ const Overlay_1 = __importDefault(require("../Overlay"));
14
+ const qanda_1 = require("../../config/qanda");
15
+ function ExpertDrawer() {
16
+ const dispatch = (0, react_redux_1.useDispatch)();
17
+ const { storyId, useStaging, title } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
18
+ const { isExpertFormOpen, expertView } = (0, react_redux_1.useSelector)((state) => state.expert);
19
+ (0, react_1.useEffect)(() => {
20
+ if (isExpertFormOpen) {
21
+ try {
22
+ o_comments_1.default.init();
23
+ }
24
+ catch (error) {
25
+ reliability_1.qandaReliability.failure('expert-load-coral-ui', error);
26
+ }
27
+ }
28
+ }, [isExpertFormOpen]);
29
+ if (!isExpertFormOpen) {
30
+ return null;
31
+ }
32
+ return ((0, jsx_runtime_1.jsx)(Overlay_1.default, { title: qanda_1.PRODUCT_NAME, closeCallback: () => dispatch((0, expertSlice_1.closeExpertForm)()), options: {
33
+ shadow: true,
34
+ allowInteraction: true,
35
+ }, children: (0, jsx_runtime_1.jsx)("div", { className: "o-comments", id: "o-comments-stream", "data-o-component": "o-comments", "data-o-comments-article-id": storyId, "data-o-comments-article-url": `https://www.ft.com/content/${storyId}`, "data-o-comments-use-staging-environment": useStaging, "data-o-comments-title": title }) }));
36
+ }
37
+ exports.default = ExpertDrawer;