@financial-times/qanda-ui 0.0.1-beta.32 → 0.0.1-beta.34

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 (28) hide show
  1. package/dist/cjs/components/Calendar/Apps.js +20 -6
  2. package/dist/cjs/components/Calendar/CalendarListItem.js +3 -1
  3. package/dist/cjs/components/EmbedFormField/index.js +8 -2
  4. package/dist/cjs/components/Expander/index.js +2 -2
  5. package/dist/cjs/components/FloatingActionBar/index.js +1 -1
  6. package/dist/cjs/components/QandaContainer.js +1 -21
  7. package/dist/cjs/components/QuestionForm/index.js +16 -1
  8. package/dist/cjs/components/Stream/index.js +22 -1
  9. package/dist/cjs/services/comments-api.js +28 -17
  10. package/dist/cjs/services/listeners-middleware.js +76 -27
  11. package/dist/cjs/store/updateComments.js +1 -1
  12. package/dist/esm/components/Calendar/Apps.js +20 -6
  13. package/dist/esm/components/Calendar/CalendarListItem.js +3 -1
  14. package/dist/esm/components/EmbedFormField/index.js +5 -2
  15. package/dist/esm/components/Expander/index.js +2 -2
  16. package/dist/esm/components/FloatingActionBar/index.js +1 -1
  17. package/dist/esm/components/QandaContainer.js +1 -21
  18. package/dist/esm/components/QuestionForm/index.js +17 -2
  19. package/dist/esm/components/Stream/index.js +22 -1
  20. package/dist/esm/services/comments-api.js +28 -17
  21. package/dist/esm/services/listeners-middleware.js +76 -27
  22. package/dist/esm/store/updateComments.js +1 -1
  23. package/dist/types/components/Calendar/Apps.d.ts +4 -2
  24. package/dist/types/components/Calendar/CalendarListItem.d.ts +1 -1
  25. package/dist/types/components/EmbedFormField/index.d.ts +3 -1
  26. package/dist/types/services/comments-api.d.ts +6 -37
  27. package/dist/types/store/index.d.ts +40 -200
  28. package/package.json +1 -1
@@ -12,29 +12,43 @@ const CalendarLink = ({ href, children, }) => {
12
12
  function itemChildren(name, icon) {
13
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
14
  }
15
- const description = (url) => `To join the ${qanda_1.PRODUCT_NAME} session, visit: ${url}`;
15
+ const description = (storyId) => `To join the ${qanda_1.PRODUCT_NAME} session, visit: https://www.ft.com/content/${storyId}`;
16
16
  const apps = [
17
17
  {
18
18
  name: 'Gmail',
19
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)}`,
20
+ getLink: ({ title, startDateTime, endDateTime, storyId }) => `https://www.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(title)}&dates=${formatDate(startDateTime)}/${formatDate(endDateTime)}&details=${encodeURIComponent(description(storyId))}&location=${encodeURIComponent(storyId)}`,
21
21
  },
22
22
  {
23
23
  name: 'Outlook',
24
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)}`,
25
+ getLink: ({ title, startDateTime, endDateTime, storyId }) => `https://outlook.live.com/calendar/0/deeplink/compose?subject=${encodeURIComponent(title)}&startdt=${startDateTime}&enddt=${endDateTime}&body=${encodeURIComponent(description(storyId))}&location=${encodeURIComponent(storyId)}`,
26
+ },
27
+ {
28
+ name: 'Apple',
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: "M19.8023 9.75502C19.6851 9.84618 17.5884 11.002 17.5884 13.5675C17.5884 16.5367 20.2548 17.5851 20.3362 17.6111C20.3232 17.6762 19.9097 19.0501 18.9297 20.4501C18.0507 21.684 17.1358 22.9115 15.7424 22.9115C14.3522 22.9115 13.994 22.1236 12.3857 22.1236C10.8197 22.1236 10.263 22.9375 8.98671 22.9375C7.71371 22.9375 6.8249 21.798 5.80259 20.398C4.62076 18.7539 3.66357 16.1981 3.66357 13.7759C3.66357 9.88525 6.25189 7.82436 8.80113 7.82436C10.1523 7.82436 11.282 8.69365 12.1317 8.69365C12.9392 8.69365 14.2024 7.77227 15.7424 7.77227C16.3251 7.77227 18.4218 7.82436 19.8023 9.75502ZM15.0098 6.12487C15.6479 5.38907 16.0972 4.36351 16.0972 3.33796C16.0972 3.19796 16.0842 3.05471 16.0582 2.9375C15.0228 2.97657 13.7889 3.61144 13.0466 4.45467C12.4638 5.10257 11.9169 6.12487 11.9169 7.16345C11.9169 7.31972 11.9462 7.476 11.9559 7.52483C12.0243 7.53786 12.1285 7.55088 12.2359 7.55088C13.1671 7.55088 14.3359 6.94531 15.0098 6.12487Z", fill: "black" }) })),
30
+ getLink: ({ storyId, useStaging, commentsAPIUrl }) => {
31
+ return `${commentsAPIUrl}/story/${storyId}/invitation${useStaging ? '?staging=1' : ''}`;
32
+ },
26
33
  },
27
34
  {
28
35
  name: 'Yahoo',
29
36
  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)}`,
37
+ getLink: ({ title, startDateTime, endDateTime, storyId }) => `https://calendar.yahoo.com/?v=60&title=${encodeURIComponent(title)}&st=${formatDate(startDateTime)}&et=${formatDate(endDateTime)}&desc=${encodeURIComponent(description(storyId))}&in_loc=${encodeURIComponent(storyId)}`,
31
38
  },
32
39
  ];
33
40
  exports.default = apps.map(({ name, icon, getLink }) => ({
34
41
  name,
35
42
  icon,
36
- link: ({ title, startDateTime, endDateTime, url, }) => {
37
- const href = getLink({ title, startDateTime, endDateTime, url });
43
+ link: ({ title, startDateTime, endDateTime, storyId, useStaging, commentsAPIUrl, }) => {
44
+ const href = getLink({
45
+ title,
46
+ startDateTime,
47
+ endDateTime,
48
+ storyId,
49
+ useStaging,
50
+ commentsAPIUrl,
51
+ });
38
52
  return (0, jsx_runtime_1.jsx)(CalendarLink, { href: href, children: itemChildren(name, icon) });
39
53
  },
40
54
  }));
@@ -17,7 +17,9 @@ function CalendarListItem({ name, link }) {
17
17
  title,
18
18
  startDateTime: data.startDateTime,
19
19
  endDateTime: data.endDateTime,
20
- url: `https://www.ft.com/content/${storyId}`,
20
+ storyId,
21
+ useStaging,
22
+ commentsAPIUrl,
21
23
  children: name,
22
24
  }) })) }));
23
25
  }
@@ -1,12 +1,17 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const jsx_runtime_1 = require("preact/jsx-runtime");
7
+ const o_tracking_1 = __importDefault(require("@financial-times/o-tracking"));
4
8
  const react_redux_1 = require("react-redux");
5
9
  const questionSlice_1 = require("../../store/questionSlice");
6
10
  const react_1 = require("react");
7
11
  const react_dom_1 = require("react-dom");
8
12
  const qanda_1 = require("../../config/qanda");
9
- function EmbedFormField({ container }) {
13
+ const tracking_1 = require("../../utils/tracking");
14
+ function EmbedFormField({ container, status = 'pre-live', }) {
10
15
  const dispatch = (0, react_redux_1.useDispatch)();
11
16
  const [portalTarget, setPortalTarget] = (0, react_1.useState)();
12
17
  (0, react_1.useEffect)(() => {
@@ -14,12 +19,13 @@ function EmbedFormField({ container }) {
14
19
  const portalContainer = document.createElement('div');
15
20
  container.prepend(portalContainer);
16
21
  setPortalTarget(portalContainer);
22
+ o_tracking_1.default.click.init('cta', '[data-component=embed-form-field]');
17
23
  return () => {
18
24
  portalContainer.remove();
19
25
  };
20
26
  }
21
27
  }, []);
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"] })] }));
28
+ 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", "data-trackable": (0, tracking_1.trackingNS)(`embed-form-field-${status}`), "data-component": "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
29
  return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, react_dom_1.createPortal)(portalElement, portalTarget || document.body) });
24
30
  }
25
31
  exports.default = EmbedFormField;
@@ -60,8 +60,8 @@ function Expander({ className = '', children = [], expandLabel = 'Expand', colla
60
60
  const componentClassName = `${expandCollapseClass} ${className}`;
61
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
62
  className: 'expander__link',
63
- 'data-trackable': (0, tracking_1.trackingNS)('expander'),
64
- 'data-trackable-context-action': label,
63
+ 'data-trackable': (0, tracking_1.trackingNS)(`expander-${state}`),
64
+ 'data-trackable-context-trigger_action': label,
65
65
  'aria-expanded': state === 'expanded' ? true : false,
66
66
  'aria-controls': expanderContentID,
67
67
  'data-action': state === 'expanded' ? 'collapse' : 'expand',
@@ -55,7 +55,7 @@ function FloatingActionBar({ isLive = true }) {
55
55
  }, label: view.description, type: "ghost", theme: "mono", icon: "edit", iconOnly: isMobile, onClick: view.onClick }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
56
56
  ariaLabel: 'Back to top',
57
57
  'data-trackable': (0, tracking_1.trackingNS)('back_to_top'),
58
- 'data-trackable-context-action': 'Back to top',
58
+ 'data-trackable-context-trigger_action': 'Back to top',
59
59
  'data-testid': 'floating-action-bar__back-to-top-button',
60
60
  }, label: "Back to top", type: "ghost", theme: "mono", icon: "scroll-to", iconOnly: isMobile, onClick: handleShowUpdates })] }), updatedComments?.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "floating-action-bar__updates-counter o3-type-detail", "aria-label": `${updatedComments.length} new ${updatedComments.length === 1 ? 'update' : 'updates'} available`, role: "status", "aria-live": "polite", children: updatedComments.length }))] })] }));
61
61
  }
@@ -17,10 +17,8 @@ const CountdownTimer_1 = __importDefault(require("./CountdownTimer"));
17
17
  const FloatingActionBar_1 = __importDefault(require("./FloatingActionBar"));
18
18
  const EmbedFormField_1 = __importDefault(require("./EmbedFormField"));
19
19
  const QandaProvider_1 = require("./QandaProvider");
20
- const o_tracking_1 = __importDefault(require("@financial-times/o-tracking"));
21
20
  const comments_api_1 = require("../services/comments-api");
22
21
  const qanda_1 = require("../config/qanda");
23
- const tracking_1 = require("../utils/tracking");
24
22
  function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
25
23
  const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
26
24
  const dispatch = (0, react_redux_1.useDispatch)();
@@ -69,24 +67,6 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
69
67
  dispatch(comments_api_1.nextCommentsApi.util.resetApiState());
70
68
  };
71
69
  }, [dispatch, storyId]);
72
- (0, react_1.useEffect)(() => {
73
- if (!data || !status)
74
- return;
75
- o_tracking_1.default.view.init({
76
- selector: '.qanda-container',
77
- getContextData: () => {
78
- return {
79
- ...tracking_1.baseTrackingData,
80
- custom: [
81
- {
82
- name: 'status',
83
- value: status,
84
- },
85
- ],
86
- };
87
- },
88
- });
89
- }, [data, status]);
90
- return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-container", children: [error && ((0, jsx_runtime_1.jsxs)("p", { children: ["We are not able to load ", qanda_1.PRODUCT_NAME, " 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, { isLive: 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 })] }));
70
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-container", children: [error && ((0, jsx_runtime_1.jsxs)("p", { children: ["We are not able to load ", qanda_1.PRODUCT_NAME, " 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, { isLive: status === 'live' })), status !== 'close' && !showExpertView && ((0, jsx_runtime_1.jsx)(EmbedFormField_1.default, { container: embedFormFieldContainer, status: status }))] })), (0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", "aria-atomic": "true", className: "o3-visually-hidden", children: ariaMessage })] }));
91
71
  }
92
72
  exports.default = QandaContainer;
@@ -7,6 +7,7 @@ exports.default = QuestionForm;
7
7
  const jsx_runtime_1 = require("preact/jsx-runtime");
8
8
  const react_redux_1 = require("react-redux");
9
9
  const classnames_1 = __importDefault(require("classnames"));
10
+ const o_tracking_1 = __importDefault(require("@financial-times/o-tracking"));
10
11
  const react_1 = require("react");
11
12
  const questionSlice_1 = require("../../store/questionSlice");
12
13
  const QandaProvider_1 = require("../QandaProvider");
@@ -34,6 +35,19 @@ function QuestionForm() {
34
35
  // you use this when you want to clear the state of the usePostQuestionMutation hook as if you had just started on the page
35
36
  // as we allow users to see things like error messages and success messages, we do not clear those until we try to send another mutation, otherwise those things will clear too early
36
37
  const [postQuestion, { isLoading, isSuccess, error, reset }] = (0, comments_api_1.usePostQuestionMutation)();
38
+ (0, react_1.useEffect)(() => {
39
+ if (isFormOpen) {
40
+ o_tracking_1.default.view.init({
41
+ selector: '.overlay__content-box',
42
+ getContextData: () => {
43
+ return {
44
+ ...tracking_1.baseTrackingData,
45
+ trigger_action: 'open_question_form',
46
+ };
47
+ },
48
+ });
49
+ }
50
+ }, [isFormOpen]);
37
51
  (0, react_1.useEffect)(() => {
38
52
  if (userDisplayNameError) {
39
53
  dispatch((0, questionSlice_1.updateDisplayNameError)('Sorry, we are unable to update display names. Please try again.'));
@@ -117,7 +131,7 @@ function QuestionForm() {
117
131
  (0, jsx_runtime_1.jsxs)(Overlay_1.default, { title: "Ask a question", closeCallback: () => {
118
132
  setNewDisplayName(null);
119
133
  dispatch((0, questionSlice_1.closeForm)());
120
- }, children: [profile.isSubscribed && ((0, jsx_runtime_1.jsx)("div", { className: "overlay__content-box overlay__form-box", "data-testid": "question-form-overlay", children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, "data-trackable": (0, tracking_1.trackingNS)(`submit_question_form_${postAnonymously ? 'anonymous' : 'displayname'}`), children: [(0, jsx_runtime_1.jsxs)("div", { className: "overlay__user-container", children: [!postAnonymously && profile?.displayName && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("span", { className: "overlay__user-container-display-name o3-type-body-highlight", children: [(0, jsx_runtime_1.jsx)("i", { className: "overlay__message-icon", style: { maskImage: 'var(--o3-icon-user)' } }), profile.displayName] }) })), !profile?.displayName && ((0, jsx_runtime_1.jsxs)("label", { for: "display-name", className: "question-form__displayname-input o-forms-field", children: [(0, jsx_runtime_1.jsx)("span", { className: "o-forms-title", children: (0, jsx_runtime_1.jsx)("span", { className: "o3-type-body-highlight", children: "Choose a display name" }) }), (0, jsx_runtime_1.jsxs)("span", { className: (0, classnames_1.default)([
134
+ }, children: [profile.isSubscribed && ((0, jsx_runtime_1.jsx)("div", { className: "overlay__content-box overlay__form-box", "data-testid": "question-form-overlay", "data-trackable-context-trigger_action": "open question form", children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, "data-trackable": (0, tracking_1.trackingNS)(`submit_question_form_${postAnonymously ? 'anonymous' : 'displayname'}`), children: [(0, jsx_runtime_1.jsxs)("div", { className: "overlay__user-container", children: [!postAnonymously && profile?.displayName && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("span", { className: "overlay__user-container-display-name o3-type-body-highlight", children: [(0, jsx_runtime_1.jsx)("i", { className: "overlay__message-icon", style: { maskImage: 'var(--o3-icon-user)' } }), profile.displayName] }) })), !profile?.displayName && ((0, jsx_runtime_1.jsxs)("label", { for: "display-name", className: "question-form__displayname-input o-forms-field", children: [(0, jsx_runtime_1.jsx)("span", { className: "o-forms-title", children: (0, jsx_runtime_1.jsx)("span", { className: "o3-type-body-highlight", children: "Choose a display name" }) }), (0, jsx_runtime_1.jsxs)("span", { className: (0, classnames_1.default)([
121
135
  'question-form__displayname__input',
122
136
  'o-forms-input',
123
137
  'o-forms-input--text',
@@ -146,6 +160,7 @@ function QuestionForm() {
146
160
  : isLoading
147
161
  ? 'Submitting...'
148
162
  : 'Submit your question',
163
+ dataTrackable: (0, tracking_1.trackingNS)('question-form__submit-button'),
149
164
  },
150
165
  // @ts-expect-error - o3 Button is currently accepting a JSX element as label
151
166
  // but the type definition needs to be updated by Origami team
@@ -6,11 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const jsx_runtime_1 = require("preact/jsx-runtime");
7
7
  const react_1 = require("react");
8
8
  const react_redux_1 = require("react-redux");
9
+ const o_tracking_1 = __importDefault(require("@financial-times/o-tracking"));
9
10
  const index_1 = __importDefault(require("../QandaBlock/index"));
10
11
  const QandaProvider_1 = require("../QandaProvider");
11
12
  const Loader_1 = __importDefault(require("../Loader"));
12
13
  const comments_api_1 = require("../../services/comments-api");
13
14
  const ErrorMessage_1 = __importDefault(require("../ErrorMessage"));
15
+ const tracking_1 = require("../../utils/tracking");
14
16
  function Stream({ status = 'pre-live' }) {
15
17
  const latestAnsweredQuestionId = (0, react_redux_1.useSelector)((state) => state.stream.latestAnsweredQuestionId);
16
18
  const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
@@ -28,7 +30,26 @@ function Stream({ status = 'pre-live' }) {
28
30
  const element = document.getElementById(latestAnsweredQuestionId);
29
31
  element?.scrollIntoView({ behavior: 'smooth' });
30
32
  }, [latestAnsweredQuestionId]);
31
- return ((0, jsx_runtime_1.jsxs)("section", { className: "qanda__stream", children: [data && data.qandas.length > 0 && ((0, jsx_runtime_1.jsx)("h2", { className: "o3-type-title-md qanda__stream-title", id: "qanda__stream-title", children: data.status && data.status === 'pre-live'
33
+ (0, react_1.useEffect)(() => {
34
+ if (!data || !status)
35
+ return;
36
+ o_tracking_1.default.view.init({
37
+ intersectionObserverThreshold: 0.1,
38
+ selector: '.qanda__stream',
39
+ getContextData: () => {
40
+ return {
41
+ ...tracking_1.baseTrackingData,
42
+ custom: [
43
+ {
44
+ name: 'status',
45
+ value: status,
46
+ },
47
+ ],
48
+ };
49
+ },
50
+ });
51
+ }, [data, status]);
52
+ return ((0, jsx_runtime_1.jsxs)("section", { className: "qanda__stream", "data-trackable-context-trigger_action": "view qanda container", children: [data && data.qandas.length > 0 && ((0, jsx_runtime_1.jsx)("h2", { className: "o3-type-title-md qanda__stream-title", id: "qanda__stream-title", children: data.status && data.status === 'pre-live'
32
53
  ? 'Top questions so far'
33
54
  : 'What our readers are asking' })), (0, jsx_runtime_1.jsxs)("div", { className: "qanda__blocks-container", "data-testid": "qanda__blocks-container", id: "qanda__blocks-container", "aria-busy": isLoading, children: [isLoading && ((0, jsx_runtime_1.jsx)(Loader_1.default, { theme: "dark", extraClassNames: "qanda__stream-loader" })), error && ((0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error, storyId: storyId, operation: "loading-stream" })), data &&
34
55
  data.qandas &&
@@ -48,18 +48,16 @@ exports.nextCommentsApi = (0, react_1.createApi)({
48
48
  console.log(`Connected to SSE: ${sseEndpoint}`);
49
49
  const state = getState();
50
50
  const lastEventId = state.stream.mostRecentUpdateTime;
51
- if (lastEventId !== null && lastEventId !== undefined) {
52
- try {
53
- await dispatch(exports.nextCommentsApi.endpoints.getCatchupUpdates.initiate({
54
- storyId: arg.storyId,
55
- useStaging: arg.useStaging,
56
- commentsAPIUrl: arg.commentsAPIUrl,
57
- lastEventId: `${lastEventId}`,
58
- }));
59
- }
60
- catch (error) {
61
- reliability_1.qandaReliability.failure('qa-updates', error);
62
- }
51
+ try {
52
+ await dispatch(exports.nextCommentsApi.endpoints.getCatchupUpdates.initiate({
53
+ storyId: arg.storyId,
54
+ useStaging: arg.useStaging,
55
+ commentsAPIUrl: arg.commentsAPIUrl,
56
+ ...(lastEventId && { lastEventId: `${lastEventId}` }),
57
+ }, { forceRefetch: true }));
58
+ }
59
+ catch (error) {
60
+ reliability_1.qandaReliability.failure('qa-updates', error);
63
61
  }
64
62
  };
65
63
  exports.eventSource.onmessage = (event) => {
@@ -117,14 +115,27 @@ exports.nextCommentsApi = (0, react_1.createApi)({
117
115
  }),
118
116
  // CATCHUP UPDATES
119
117
  getCatchupUpdates: builder.query({
120
- query: ({ storyId, useStaging, commentsAPIUrl, lastEventId, }) => `${commentsAPIUrl}/story/${storyId}/catchup-updates?${useStaging ? 'staging=1&' : ''}${`lastEventId=${lastEventId}`}`,
118
+ query: ({ storyId, useStaging, commentsAPIUrl, lastEventId, }) => {
119
+ const params = new URLSearchParams();
120
+ if (useStaging) {
121
+ params.append('staging', '1');
122
+ }
123
+ if (lastEventId) {
124
+ params.append('lastEventId', lastEventId);
125
+ }
126
+ const queryString = params.toString();
127
+ return `${commentsAPIUrl}/story/${storyId}/catchup-updates${queryString ? `?${queryString}` : ''}`;
128
+ },
121
129
  transformResponse: (response) => {
122
- const updatedComments = response?.comments.map((comment) => {
123
- return comment.data;
124
- });
130
+ const updatedComments = Array.isArray(response?.comments)
131
+ ? response.comments.map((comment) => {
132
+ return comment.data;
133
+ })
134
+ : [];
125
135
  return {
126
136
  updatedComments,
127
- lastEventId: response?.lastEventId,
137
+ status: response?.status ?? null,
138
+ lastEventId: response?.lastEventId ?? null,
128
139
  };
129
140
  },
130
141
  }),
@@ -18,7 +18,7 @@ exports.listenerMiddleware = (0, toolkit_1.createListenerMiddleware)();
18
18
  * Calculates the most recent update timestamp and answer id from a Q&A question
19
19
  *
20
20
  * @param qandas - A Comment object representing a question with nested answers
21
- * @returns Object containing mostRecentUpdateTime (timestamp) and mostRecentUpdateId (answer id)
21
+ * @returns Object containing mostRecentUpdateTime (answer timestamp) and mostRecentUpdateId (answer id)
22
22
  *
23
23
  * @example
24
24
  * const { mostRecentUpdateTime, mostRecentUpdateId } = getMostRecentUpdate(qandas);
@@ -38,6 +38,28 @@ function getMostRecentUpdate(qandaComment) {
38
38
  }
39
39
  return { mostRecentUpdateTime, mostRecentUpdateId };
40
40
  }
41
+ /**
42
+ * Shallow compares two StreamStatus objects to determine if they are identical
43
+ *
44
+ * Performs a shallow comparison of all properties between the previous and new status
45
+ * entries. Used to prevent unnecessary updates when the status hasn't actually changed.
46
+ *
47
+ * @param previousEntry - The previous StreamStatus object to compare
48
+ * @param newEntry - The new StreamStatus object to compare against
49
+ * @returns True if all properties are identical, false otherwise
50
+ */
51
+ function isSameStatus(previousEntry, newEntry) {
52
+ if (Object.keys(previousEntry).length !== Object.keys(newEntry).length) {
53
+ return false;
54
+ }
55
+ for (const key in previousEntry) {
56
+ const typedKey = key;
57
+ if (previousEntry[typedKey] !== newEntry[typedKey]) {
58
+ return false;
59
+ }
60
+ }
61
+ return true;
62
+ }
41
63
  /**
42
64
  * Listener: Updates most recent timestamp and initiates catchup when live Q&A stream data is fetched
43
65
  *
@@ -51,12 +73,17 @@ function getMostRecentUpdate(qandaComment) {
51
73
  * @dispatches setStreamStartMostRecentAnswerId - Updates stream.streamStartMostRecentAnswerId in Redux store
52
74
  * @dispatches nextCommentsApi.endpoints.getCatchupUpdates.initiate - Triggers catchup request for missed updates
53
75
  */
76
+ // TODO: consider when this should run now that we have state changes too.
77
+ // consider that it will run on every live stream query, not just the first one.
78
+ // therefore it will happen on state change, and on first load maybe twice.
54
79
  exports.listenerMiddleware.startListening({
55
80
  matcher: (0, toolkit_1.isAnyOf)(comments_api_1.nextCommentsApi.endpoints.getQandAStream.matchFulfilled),
56
81
  effect: (action, listenerApi) => {
82
+ const state = listenerApi.getState();
57
83
  const { payload, meta } = action;
58
84
  const { storyId, useStaging, commentsAPIUrl, status } = meta.arg.originalArgs;
59
- // Check if this is a live stream QUERY, not response. If it is a live query we get questions and answers.
85
+ // Check if this is a live stream QUERY, not response. If it is a live QUERY we will get questions and answers. This will fire when stream starts in live mode or switches to live mode for first time.
86
+ const isPreLiveStream = status === 'pre-live';
60
87
  const isLiveStream = status === 'live';
61
88
  if (isLiveStream &&
62
89
  Array.isArray(payload.qandas) &&
@@ -68,54 +95,76 @@ exports.listenerMiddleware.startListening({
68
95
  listenerApi.dispatch((0, stream_1.setMostRecentUpdateTime)(`${mostRecentUpdateTime}`));
69
96
  listenerApi.dispatch((0, stream_1.setStreamLoadMostRecentAnswerId)(mostRecentUpdateId));
70
97
  }
98
+ }
99
+ const { mostRecentUpdateTime } = state.stream;
100
+ if (isPreLiveStream || isLiveStream) {
71
101
  listenerApi.dispatch(comments_api_1.nextCommentsApi.endpoints.getCatchupUpdates.initiate({
72
- storyId: storyId,
73
- useStaging: useStaging,
74
- commentsAPIUrl: commentsAPIUrl,
75
- lastEventId: `${mostRecentUpdateTime}`,
102
+ storyId,
103
+ useStaging,
104
+ commentsAPIUrl,
105
+ ...(mostRecentUpdateTime && {
106
+ lastEventId: `${mostRecentUpdateTime}`,
107
+ }),
76
108
  }));
77
109
  }
78
110
  },
79
111
  });
80
112
  /**
81
- * Listener: Updates updates cache and most recent update when catchup comments are received
113
+ * Listener: Processes successful catchup updates and manages comment/status updates
82
114
  *
83
- * Responds to successful getCatchupUpdates queries and adds them to the updates list.
84
- * Filters out comments that match the most recent update from the stream to avoid
85
- * duplicates caused by latency between coral and valkey stream updates.
115
+ * Responds to successful getCatchupUpdates queries and processes the response:
116
+ * - Adds new comments to the updates cache, filtering out duplicates
117
+ * - Updates status when changes are detected compared to the last status
118
+ * - Updates the most recent timestamp for future catchup requests
86
119
  *
87
120
  * @listens nextCommentsApi.endpoints.getCatchupUpdates.matchFulfilled
88
121
  * @dispatches nextCommentsApi.util.updateQueryData - Updates the cache of updated comments in the store
89
122
  * @dispatches setMostRecentUpdateTime - Updates stream.mostRecentUpdateTime with new lastEventId
123
+ * @dispatches nextCommentsApi.util.updateQueryData - Updates the status if it has changed
90
124
  */
91
125
  exports.listenerMiddleware.startListening({
92
126
  matcher: comments_api_1.nextCommentsApi.endpoints.getCatchupUpdates.matchFulfilled,
93
127
  effect: (action, listenerApi) => {
94
128
  const { payload, meta } = action;
95
129
  const { storyId, useStaging, commentsAPIUrl } = meta.arg.originalArgs;
96
- const { updatedComments, lastEventId } = payload;
97
- if (!updatedComments)
130
+ const { updatedComments, status, lastEventId = null } = payload;
131
+ if (!updatedComments && !status)
98
132
  return;
99
- console.log('Received catchup updates, new event id:', lastEventId);
100
- const updateType = comments_api_1.updateTypes['QA_NEW_REPLY'];
101
133
  const state = listenerApi.getState();
102
- const { streamLoadMostRecentAnswerId, mostRecentUpdateTime: mostRecentInStreamTime, } = state.stream;
103
- // filter out catchup comments that are the same as the most recent update in the stream list, which can happen on first load
104
- // there is latency between the comment being saved in coral and then added to our valkey stream. Updates after this will all come from valkey.
105
- const filteredComments = updatedComments.filter((comment) => {
106
- const { mostRecentUpdateId, mostRecentUpdateTime } = getMostRecentUpdate(comment);
107
- if (streamLoadMostRecentAnswerId === mostRecentUpdateId &&
108
- mostRecentInStreamTime === `${mostRecentUpdateTime}`) {
109
- return false;
134
+ // check for new updated comments
135
+ if (Array.isArray(updatedComments) && updatedComments.length > 0) {
136
+ const updateType = comments_api_1.updateTypes['QA_NEW_REPLY'];
137
+ const { streamLoadMostRecentAnswerId, mostRecentUpdateTime: mostRecentInStreamTime, } = state.stream;
138
+ // filter out catchup comments that are the same as the most recent update in the stream list, which can happen on first load when the valkey and coral response timestamps don't match
139
+ // there is latency between the comment being saved in coral and then added to our valkey stream. Updates after this will all come from valkey.
140
+ const filteredComments = updatedComments.filter((comment) => {
141
+ const { mostRecentUpdateId, mostRecentUpdateTime } = getMostRecentUpdate(comment);
142
+ if (streamLoadMostRecentAnswerId === mostRecentUpdateId &&
143
+ mostRecentInStreamTime === `${mostRecentUpdateTime}`) {
144
+ return false;
145
+ }
146
+ return true;
147
+ });
148
+ if (Array.isArray(filteredComments) && filteredComments.length) {
149
+ listenerApi.dispatch(comments_api_1.nextCommentsApi.util.updateQueryData('getQandAUpdates', { storyId, useStaging, commentsAPIUrl }, (draft) => {
150
+ draft[updateType] = draft[updateType] || [];
151
+ draft[updateType].push(...filteredComments);
152
+ }));
110
153
  }
111
- return true;
112
- });
113
- if (Array.isArray(filteredComments) && filteredComments.length) {
154
+ listenerApi.dispatch((0, stream_1.setMostRecentUpdateTime)(lastEventId));
155
+ }
156
+ // check for status updates
157
+ if (status) {
158
+ const updateType = comments_api_1.updateTypes['QA_UPDATE'];
114
159
  listenerApi.dispatch(comments_api_1.nextCommentsApi.util.updateQueryData('getQandAUpdates', { storyId, useStaging, commentsAPIUrl }, (draft) => {
115
160
  draft[updateType] = draft[updateType] || [];
116
- draft[updateType].push(...filteredComments);
161
+ // in order to avoid unnecessary updates, we check if the last update is identical to the new status. we only push changed status updates.
162
+ if (draft[updateType].length > 0 &&
163
+ isSameStatus(draft[updateType].at(-1), status)) {
164
+ return;
165
+ }
166
+ draft[updateType] = [status];
117
167
  }));
118
168
  }
119
- listenerApi.dispatch((0, stream_1.setMostRecentUpdateTime)(lastEventId));
120
169
  },
121
170
  });
@@ -34,7 +34,7 @@ const updateComments = (dispatch, updatedComments, storyId, useStaging, comments
34
34
  dispatch((0, stream_1.setLatestAnsweredQuestionId)(latestAnsweredQuestionId));
35
35
  }
36
36
  catch (error) {
37
- console.log('error', error);
37
+ console.error('error', error);
38
38
  }
39
39
  };
40
40
  exports.updateComments = updateComments;
@@ -10,29 +10,43 @@ const CalendarLink = ({ href, children, }) => {
10
10
  function itemChildren(name, icon) {
11
11
  return (_jsxs(_Fragment, { children: [_jsx("span", { className: "live-qa-calendar-list__item__icon", children: icon }), _jsx("span", { className: "live-qa-calendar-list__item__text", children: name })] }));
12
12
  }
13
- const description = (url) => `To join the ${PRODUCT_NAME} session, visit: ${url}`;
13
+ const description = (storyId) => `To join the ${PRODUCT_NAME} session, visit: https://www.ft.com/content/${storyId}`;
14
14
  const apps = [
15
15
  {
16
16
  name: 'Gmail',
17
17
  icon: (_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: _jsxs("g", { transform: "translate(4, 3.36)", children: [_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" }), _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" }), _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" }), _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" })] }) })),
18
- 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)}`,
18
+ getLink: ({ title, startDateTime, endDateTime, storyId }) => `https://www.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(title)}&dates=${formatDate(startDateTime)}/${formatDate(endDateTime)}&details=${encodeURIComponent(description(storyId))}&location=${encodeURIComponent(storyId)}`,
19
19
  },
20
20
  {
21
21
  name: 'Outlook',
22
22
  icon: (_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: _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" }) })),
23
- 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)}`,
23
+ getLink: ({ title, startDateTime, endDateTime, storyId }) => `https://outlook.live.com/calendar/0/deeplink/compose?subject=${encodeURIComponent(title)}&startdt=${startDateTime}&enddt=${endDateTime}&body=${encodeURIComponent(description(storyId))}&location=${encodeURIComponent(storyId)}`,
24
+ },
25
+ {
26
+ name: 'Apple',
27
+ icon: (_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: _jsx("path", { d: "M19.8023 9.75502C19.6851 9.84618 17.5884 11.002 17.5884 13.5675C17.5884 16.5367 20.2548 17.5851 20.3362 17.6111C20.3232 17.6762 19.9097 19.0501 18.9297 20.4501C18.0507 21.684 17.1358 22.9115 15.7424 22.9115C14.3522 22.9115 13.994 22.1236 12.3857 22.1236C10.8197 22.1236 10.263 22.9375 8.98671 22.9375C7.71371 22.9375 6.8249 21.798 5.80259 20.398C4.62076 18.7539 3.66357 16.1981 3.66357 13.7759C3.66357 9.88525 6.25189 7.82436 8.80113 7.82436C10.1523 7.82436 11.282 8.69365 12.1317 8.69365C12.9392 8.69365 14.2024 7.77227 15.7424 7.77227C16.3251 7.77227 18.4218 7.82436 19.8023 9.75502ZM15.0098 6.12487C15.6479 5.38907 16.0972 4.36351 16.0972 3.33796C16.0972 3.19796 16.0842 3.05471 16.0582 2.9375C15.0228 2.97657 13.7889 3.61144 13.0466 4.45467C12.4638 5.10257 11.9169 6.12487 11.9169 7.16345C11.9169 7.31972 11.9462 7.476 11.9559 7.52483C12.0243 7.53786 12.1285 7.55088 12.2359 7.55088C13.1671 7.55088 14.3359 6.94531 15.0098 6.12487Z", fill: "black" }) })),
28
+ getLink: ({ storyId, useStaging, commentsAPIUrl }) => {
29
+ return `${commentsAPIUrl}/story/${storyId}/invitation${useStaging ? '?staging=1' : ''}`;
30
+ },
24
31
  },
25
32
  {
26
33
  name: 'Yahoo',
27
34
  icon: (_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: _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" }) })),
28
- 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)}`,
35
+ getLink: ({ title, startDateTime, endDateTime, storyId }) => `https://calendar.yahoo.com/?v=60&title=${encodeURIComponent(title)}&st=${formatDate(startDateTime)}&et=${formatDate(endDateTime)}&desc=${encodeURIComponent(description(storyId))}&in_loc=${encodeURIComponent(storyId)}`,
29
36
  },
30
37
  ];
31
38
  export default apps.map(({ name, icon, getLink }) => ({
32
39
  name,
33
40
  icon,
34
- link: ({ title, startDateTime, endDateTime, url, }) => {
35
- const href = getLink({ title, startDateTime, endDateTime, url });
41
+ link: ({ title, startDateTime, endDateTime, storyId, useStaging, commentsAPIUrl, }) => {
42
+ const href = getLink({
43
+ title,
44
+ startDateTime,
45
+ endDateTime,
46
+ storyId,
47
+ useStaging,
48
+ commentsAPIUrl,
49
+ });
36
50
  return _jsx(CalendarLink, { href: href, children: itemChildren(name, icon) });
37
51
  },
38
52
  }));
@@ -15,7 +15,9 @@ function CalendarListItem({ name, link }) {
15
15
  title,
16
16
  startDateTime: data.startDateTime,
17
17
  endDateTime: data.endDateTime,
18
- url: `https://www.ft.com/content/${storyId}`,
18
+ storyId,
19
+ useStaging,
20
+ commentsAPIUrl,
19
21
  children: name,
20
22
  }) })) }));
21
23
  }
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
+ import oTracking from '@financial-times/o-tracking';
2
3
  import { useDispatch } from 'react-redux';
3
4
  import { openForm } from '../../store/questionSlice';
4
5
  import { useEffect, useState } from 'react';
5
6
  import { createPortal } from 'react-dom';
6
7
  import { PRODUCT_NAME } from '../../config/qanda';
7
- function EmbedFormField({ container }) {
8
+ import { trackingNS } from '../../utils/tracking';
9
+ function EmbedFormField({ container, status = 'pre-live', }) {
8
10
  const dispatch = useDispatch();
9
11
  const [portalTarget, setPortalTarget] = useState();
10
12
  useEffect(() => {
@@ -12,12 +14,13 @@ function EmbedFormField({ container }) {
12
14
  const portalContainer = document.createElement('div');
13
15
  container.prepend(portalContainer);
14
16
  setPortalTarget(portalContainer);
17
+ oTracking.click.init('cta', '[data-component=embed-form-field]');
15
18
  return () => {
16
19
  portalContainer.remove();
17
20
  };
18
21
  }
19
22
  }, []);
20
- const portalElement = (_jsxs("div", { className: "qanda-portal embed-form-field__container o3-type-body-highlight", children: ["Submit your question for ", PRODUCT_NAME, _jsxs("div", { onClick: () => dispatch(openForm()), className: "embed-form-field__text-input o3-type-body-lg", "data-testid": "embed-form-field", children: [_jsx("span", { className: "embed-form-field__icon", children: _jsx("i", { style: { maskImage: 'var(--o3-icon-user)' } }) }), "Enter your question"] })] }));
23
+ const portalElement = (_jsxs("div", { className: "qanda-portal embed-form-field__container o3-type-body-highlight", children: ["Submit your question for ", PRODUCT_NAME, _jsxs("div", { onClick: () => dispatch(openForm()), className: "embed-form-field__text-input o3-type-body-lg", "data-testid": "embed-form-field", "data-trackable": trackingNS(`embed-form-field-${status}`), "data-component": "embed-form-field", children: [_jsx("span", { className: "embed-form-field__icon", children: _jsx("i", { style: { maskImage: 'var(--o3-icon-user)' } }) }), "Enter your question"] })] }));
21
24
  return _jsx(_Fragment, { children: createPortal(portalElement, portalTarget || document.body) });
22
25
  }
23
26
  export default EmbedFormField;