@financial-times/qanda-ui 0.0.1-beta.1

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 (57) hide show
  1. package/.prettierrc +3 -0
  2. package/.toolkitrc.yml +34 -0
  3. package/.vscode/settings.json +4 -0
  4. package/CODEOWNERS +3 -0
  5. package/README.md +161 -0
  6. package/babel.config.json +10 -0
  7. package/dist/index.html +4 -0
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.LICENSE.txt +15 -0
  10. package/dist/mockServiceWorker.js +307 -0
  11. package/jest.setup.ts +15 -0
  12. package/package.json +115 -0
  13. package/server.js +17 -0
  14. package/src/client/styles.scss +61 -0
  15. package/src/components/Author.tsx +45 -0
  16. package/src/components/CountdownTimer.tsx +73 -0
  17. package/src/components/Network.tsx +19 -0
  18. package/src/components/Qanda.tsx +51 -0
  19. package/src/components/QandaBlock.tsx +63 -0
  20. package/src/components/QandaContainer.tsx +60 -0
  21. package/src/components/QandaOComments.tsx +60 -0
  22. package/src/components/QandaProvider.tsx +13 -0
  23. package/src/components/QuestionForm.tsx +122 -0
  24. package/src/components/Status.tsx +20 -0
  25. package/src/components/Stream.tsx +49 -0
  26. package/src/components/UITestBar.tsx +76 -0
  27. package/src/components/UpdatesButton.tsx +45 -0
  28. package/src/components/UserInfo.tsx +57 -0
  29. package/src/index.tsx +58 -0
  30. package/src/services/comments-api.ts +217 -0
  31. package/src/services/custom-sse.ts +101 -0
  32. package/src/services/mocks/api-endpoints.ts +36 -0
  33. package/src/services/mocks/mock-comments.json +307 -0
  34. package/src/services/mocks/msw-browser.ts +4 -0
  35. package/src/services/mocks/msw-server.ts +4 -0
  36. package/src/store/index.ts +41 -0
  37. package/src/store/network.ts +23 -0
  38. package/src/store/qa.ts +22 -0
  39. package/src/store/questionSlice.ts +37 -0
  40. package/src/store/stream.ts +21 -0
  41. package/src/store/updateComments.ts +60 -0
  42. package/src/store/user.ts +55 -0
  43. package/src/types/async-operation.d.ts +4 -0
  44. package/src/types/comment.d.ts +19 -0
  45. package/src/types/globals.d.ts +1 -0
  46. package/src/types/modules.d.ts +1 -0
  47. package/src/types/o-types.d.ts +2 -0
  48. package/src/types/qanda.d.ts +7 -0
  49. package/src/types/sse-events.d.ts +6 -0
  50. package/src/types/x-dash.d.ts +6 -0
  51. package/src/utils/auth.ts +9 -0
  52. package/src/utils/mocks/update.ts +168 -0
  53. package/src/utils/qandas.ts +21 -0
  54. package/tsconfig.json +26 -0
  55. package/webpack.common.js +60 -0
  56. package/webpack.dev.js +26 -0
  57. package/webpack.prod.js +6 -0
@@ -0,0 +1,51 @@
1
+ import { Provider } from 'react-redux';
2
+ import { store } from '../store';
3
+ import QandaContainer from './QandaContainer';
4
+ import { QandaContext } from './QandaProvider';
5
+ import '../client/styles.scss';
6
+
7
+ import type { QandaProps } from '../types/qanda';
8
+
9
+ function Qanda({
10
+ storyId,
11
+ useStaging,
12
+ title,
13
+ commentsAPIHost,
14
+ commentsAPIVersion,
15
+ }: QandaProps) {
16
+ // check we have a valid ft.com url and remove any trailing slashes
17
+ const allowedHostRegx = /^[\w\-\.]+\.ft\.com$/i;
18
+ const allowedVersionRegx = /^v\d+$/i;
19
+ const validateApiUrl = (url: string, version: string): string | undefined => {
20
+ const baseURL = new URL(url);
21
+ if (
22
+ !allowedHostRegx.test(baseURL.hostname) ||
23
+ !allowedVersionRegx.test(version)
24
+ ) {
25
+ console.error('Invalid comments API details provided');
26
+ return;
27
+ }
28
+ return `https://${baseURL.hostname}/qa/${version}`;
29
+ };
30
+ const cleanCommentsAPIUrl = validateApiUrl(
31
+ commentsAPIHost,
32
+ commentsAPIVersion,
33
+ );
34
+
35
+ if (!storyId || !title || !cleanCommentsAPIUrl) {
36
+ console.error('Missing required props');
37
+ return null;
38
+ }
39
+
40
+ return (
41
+ <Provider store={store}>
42
+ <QandaContext.Provider
43
+ value={{ storyId, useStaging, commentsAPIUrl: cleanCommentsAPIUrl }}
44
+ >
45
+ <QandaContainer title={title} />
46
+ </QandaContext.Provider>
47
+ </Provider>
48
+ );
49
+ }
50
+
51
+ export default Qanda;
@@ -0,0 +1,63 @@
1
+ import { RichText, Byline } from '@financial-times/cp-content-pipeline-ui';
2
+ // TODO: this component needs exporting from cp-content-pipeline-ui
3
+ import Timestamp from '@financial-times/cp-content-pipeline-ui/lib/components/LiveBlogPost/Timestamp';
4
+ import type { Comment } from '../types/comment';
5
+ import Author from './Author';
6
+
7
+ type UpdatedDateProps = {
8
+ answers: Comment[];
9
+ };
10
+
11
+ function UpdatedDate({ answers }: UpdatedDateProps) {
12
+ const sortedAnswers = Array.from(answers).sort(
13
+ (a, b) =>
14
+ new Date(b.publishedDate).getTime() - new Date(a.publishedDate).getTime(),
15
+ );
16
+ return (
17
+ <span>
18
+ <Timestamp publishedTimestamp={sortedAnswers[0].publishedDate} />
19
+ </span>
20
+ );
21
+ }
22
+
23
+ function QandaBlock({ id, body, children, byline }: Comment) {
24
+ return (
25
+ <div className="qanda-block">
26
+ <div
27
+ className="qanda-block__question"
28
+ id={id}
29
+ key={id}
30
+ data-testid={`qanda-block__question-${id}`}
31
+ >
32
+ Question
33
+ {children && <UpdatedDate answers={children} />}
34
+ <Byline structuredContent={byline} showEditedBy={false} />
35
+ <RichText structuredContent={body.structured} />
36
+ </div>
37
+ {children?.map(
38
+ (child) =>
39
+ child.body && (
40
+ <div
41
+ className="qanda-block__answer"
42
+ key={child.id}
43
+ data-testid={`qanda-block__answer-${child.id}`}
44
+ >
45
+ Answer
46
+ <Byline structuredContent={child.byline} showEditedBy={false} />
47
+ {child.author && (
48
+ <Author
49
+ role={child.author.role}
50
+ headshot={child.author.headshot}
51
+ prefLabel={child.author.prefLabel}
52
+ streamPage={child.author.streamPage}
53
+ />
54
+ )}
55
+ <RichText structuredContent={child.body.structured} />
56
+ </div>
57
+ ),
58
+ )}
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export default QandaBlock;
@@ -0,0 +1,60 @@
1
+ import { useEffect, useContext } from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+ import { getUserProfile } from '../store/user';
4
+ import { AppDispatch } from '../store';
5
+ import Status from './Status';
6
+ import UserInfo from './UserInfo';
7
+ import QandaOComments from './QandaOComments';
8
+ import Stream from './Stream';
9
+ import Network from './Network';
10
+ import UpdatesButton from './UpdatesButton';
11
+ import UITest from './UITestBar';
12
+ import CountdownTimer from './CountdownTimer';
13
+ import {
14
+ useGetQandAUpdatesQuery,
15
+ nextCommentsApi,
16
+ } from '../services/comments-api';
17
+
18
+ import { QandaContext } from './QandaProvider';
19
+
20
+ function QandaContainer({ title }: { title: string }) {
21
+ const { storyId, useStaging, commentsAPIUrl } = useContext(QandaContext);
22
+ const dispatch = useDispatch<AppDispatch>();
23
+
24
+ const { data, error, isLoading } = useGetQandAUpdatesQuery({
25
+ storyId,
26
+ useStaging,
27
+ commentsAPIUrl,
28
+ });
29
+
30
+ useEffect(() => {
31
+ dispatch(getUserProfile({ useStaging }));
32
+ return () => {
33
+ // ensure we reset the api state when the component is unmounted
34
+ dispatch(nextCommentsApi.util.resetApiState());
35
+ };
36
+ }, [dispatch, storyId]);
37
+
38
+ return (
39
+ <div className="qanda-container">
40
+ {isLoading && <p>Loading updates</p>}
41
+ {error && (
42
+ <p>
43
+ Sorry, we could not connect you to get automatic updates. Error:
44
+ {error}
45
+ </p>
46
+ )}
47
+ {data && <p>Updates: {JSON.stringify(data)}</p>}
48
+ <Status />
49
+ <Network />
50
+ <UITest />
51
+ <CountdownTimer />
52
+ <UpdatesButton />
53
+ <Stream />
54
+ <UserInfo />
55
+ <QandaOComments title={title} />
56
+ </div>
57
+ );
58
+ }
59
+
60
+ export default QandaContainer;
@@ -0,0 +1,60 @@
1
+ import OComments from '@financial-times/o-comments';
2
+ import { useEffect, useContext } from 'react';
3
+ import { useSelector } from 'react-redux';
4
+ import { RootState } from '../store';
5
+ import { QandaContext } from './QandaProvider';
6
+ import { useGetUserRolesQuery } from '../services/comments-api';
7
+
8
+ type QandaOCommentsProps = {
9
+ title: string;
10
+ };
11
+
12
+ function QandaOComments({ title }: QandaOCommentsProps) {
13
+ const allowedRoles = ['ADMIN', 'MODERATOR'];
14
+ const { storyId, useStaging, commentsAPIUrl } = useContext(QandaContext);
15
+
16
+ const profile = useSelector((state: RootState) => state.user.profile);
17
+ if (!profile) {
18
+ return null;
19
+ }
20
+
21
+ const {
22
+ data: userRole,
23
+ error,
24
+ isLoading,
25
+ } = useGetUserRolesQuery(
26
+ {
27
+ storyId,
28
+ useStaging,
29
+ commentsAPIUrl,
30
+ token: profile.token,
31
+ },
32
+ { skip: !profile.token },
33
+ );
34
+
35
+ useEffect(() => {
36
+ if (!userRole) {
37
+ return;
38
+ }
39
+ const { isExpert, isStaffExpert, role } = userRole;
40
+ const showCoral =
41
+ isExpert || isStaffExpert || (role && allowedRoles.includes(role));
42
+ if (showCoral) {
43
+ OComments.init();
44
+ }
45
+ }, [userRole]);
46
+
47
+ return (
48
+ <div
49
+ className="o-comments"
50
+ id="o-comments-stream"
51
+ data-o-component="o-comments"
52
+ data-o-comments-article-id={storyId}
53
+ data-o-comments-article-url={`https://www.ft.com/content/${storyId}`} // do not use window.location in case we have a vanity url
54
+ data-o-comments-use-staging-environment={useStaging}
55
+ data-o-comments-title={title}
56
+ />
57
+ );
58
+ }
59
+
60
+ export default QandaOComments;
@@ -0,0 +1,13 @@
1
+ import { createContext } from 'react';
2
+
3
+ type QandaContextType = {
4
+ storyId: string;
5
+ useStaging: boolean;
6
+ commentsAPIUrl: string;
7
+ };
8
+
9
+ export const QandaContext = createContext<QandaContextType>({
10
+ storyId: '',
11
+ useStaging: true,
12
+ commentsAPIUrl: '',
13
+ });
@@ -0,0 +1,122 @@
1
+ import { useDispatch, useSelector } from 'react-redux';
2
+ import type { RootState } from '../store';
3
+ import { useState, useContext, FormEvent } from 'react';
4
+ import {
5
+ openForm,
6
+ closeForm,
7
+ updateQuestion,
8
+ closeSuccess,
9
+ } from '../store/questionSlice';
10
+ import type { NetworkStatus } from '../store/network';
11
+ import { QandaContext } from './QandaProvider';
12
+ import { usePostQuestionMutation } from '../services/comments-api';
13
+
14
+ export default function QuestionForm() {
15
+ const dispatch = useDispatch();
16
+ const { storyId, useStaging, commentsAPIUrl } = useContext(QandaContext);
17
+ const [postError, setPostError] = useState<string | null>(null);
18
+ const { isFormOpen, question } = useSelector(
19
+ (state: RootState) => state.question,
20
+ );
21
+ const profile = useSelector((state: RootState) => state.user.profile);
22
+ const networkStatus = useSelector((state: RootState) => state.network.status);
23
+
24
+ const [postQuestion, { isLoading, isSuccess, error, reset }] =
25
+ usePostQuestionMutation();
26
+
27
+ const handleSubmit = async (e: FormEvent) => {
28
+ e.preventDefault();
29
+
30
+ if (!profile?.token) {
31
+ //TODO: how to handle users that are not logged in? design awaited
32
+ return;
33
+ }
34
+
35
+ try {
36
+ const response = await postQuestion({
37
+ token: profile?.token,
38
+ storyId,
39
+ useStaging,
40
+ commentsAPIUrl,
41
+ question: question,
42
+ }).unwrap();
43
+ } catch (e) {
44
+ dispatch(openForm());
45
+ setPostError('Failed to submit question');
46
+ }
47
+ };
48
+
49
+ if (isSuccess) {
50
+ return (
51
+ <div>
52
+ <p>Question submitted successfully!</p>
53
+ <button
54
+ type="button"
55
+ onClick={() => {
56
+ dispatch(closeSuccess());
57
+ reset();
58
+ }}
59
+ >
60
+ Close
61
+ </button>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ if (!isFormOpen) {
67
+ return (
68
+ <>
69
+ {networkStatus === 'offline' && (
70
+ <p>You cannot submit question while offline</p>
71
+ )}
72
+ <button
73
+ type="button"
74
+ onClick={() => dispatch(openForm())}
75
+ disabled={networkStatus === ('offline' as NetworkStatus)}
76
+ >
77
+ Ask question
78
+ </button>
79
+ </>
80
+ );
81
+ }
82
+
83
+ return (
84
+ <form onSubmit={handleSubmit}>
85
+ {error && (
86
+ <p role="alert" id="question-error-message">
87
+ {error}
88
+ </p>
89
+ )}
90
+ <textarea
91
+ value={question}
92
+ onChange={(e) => dispatch(updateQuestion(e.currentTarget.value))}
93
+ aria-label="Your question"
94
+ placeholder="Type your question"
95
+ maxLength={2000}
96
+ aria-invalid={postError ? 'true' : undefined}
97
+ aria-describedby={postError ? 'question-error-message' : undefined}
98
+ />
99
+ <div>
100
+ {!profile?.token && <p>You must be logged in to submit a question</p>}
101
+ <button
102
+ type="submit"
103
+ disabled={question.length < 20 || isLoading}
104
+ aria-label={
105
+ question.length < 20
106
+ ? 'Submit button (disabled until question is at least 20 characters)'
107
+ : 'Submit question'
108
+ }
109
+ >
110
+ Submit
111
+ </button>
112
+ <button
113
+ type="button"
114
+ onClick={() => dispatch(closeForm())}
115
+ aria-label="Close form"
116
+ >
117
+ Close
118
+ </button>
119
+ </div>
120
+ </form>
121
+ );
122
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import { RootState } from '../store';
4
+ import QuestionForm from './QuestionForm';
5
+
6
+ function Status() {
7
+ const status = useSelector((state: RootState) => state.qa.status);
8
+
9
+ return (
10
+ <div>
11
+ <p>
12
+ The Q&A is&nbsp;
13
+ <b>{status === 'live' ? 'Live' : 'Close'}</b>
14
+ </p>
15
+ {status === 'live' ? <QuestionForm /> : <p>Questions not accepted</p>}
16
+ </div>
17
+ );
18
+ }
19
+
20
+ export default Status;
@@ -0,0 +1,49 @@
1
+ import { useEffect, useContext } from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import { RootState } from '../store';
4
+ import QandaBlock from './QandaBlock';
5
+ import { QandaContext } from './QandaProvider';
6
+ import { useGetQandAStreamQuery } from '../services/comments-api';
7
+ function Stream() {
8
+ const latestAnsweredQuestionId = useSelector(
9
+ (state: RootState) => state.stream.latestAnsweredQuestionId,
10
+ );
11
+ const { storyId, useStaging, commentsAPIUrl } = useContext(QandaContext);
12
+
13
+ const { data, error, isLoading } = useGetQandAStreamQuery({
14
+ storyId,
15
+ useStaging,
16
+ commentsAPIUrl,
17
+ });
18
+
19
+ useEffect(() => {
20
+ const element = document.getElementById(latestAnsweredQuestionId);
21
+ element?.scrollIntoView({ behavior: 'smooth' });
22
+ }, [latestAnsweredQuestionId]);
23
+
24
+ return (
25
+ <div className="qanda-block" data-testid="qanda-block">
26
+ <h2>Q&As:</h2>
27
+ {isLoading && <p>Loading Stream for you, be patient...</p>}
28
+ {error && <p>Failed to load Q&A stream</p>}
29
+ {data &&
30
+ data.qandas &&
31
+ data.qandas.map((qandaBlock: any) => (
32
+ <div key={qandaBlock.id}>
33
+ <QandaBlock
34
+ id={qandaBlock.id}
35
+ body={qandaBlock.body}
36
+ type="comment"
37
+ publishedDate={qandaBlock.publishedDate}
38
+ author={qandaBlock.author}
39
+ byline={qandaBlock.byline}
40
+ >
41
+ {qandaBlock.children}
42
+ </QandaBlock>
43
+ </div>
44
+ ))}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export default Stream;
@@ -0,0 +1,76 @@
1
+ import { useDispatch, useSelector } from 'react-redux';
2
+ import { useEffect } from 'preact/hooks';
3
+ import { AppDispatch, RootState } from '../store';
4
+ import { eventSource } from '../services/comments-api';
5
+ import newAnserEventData from '../utils/mocks/update';
6
+ import type { NewAnswerEvent } from '../types/sse-events';
7
+
8
+ interface ExtendedEventSource extends EventSource {
9
+ sendMessage?: (message: NewAnswerEvent) => void;
10
+ }
11
+
12
+ function UITest() {
13
+ const dispatch = useDispatch<AppDispatch>();
14
+ const networkStatus = useSelector((state: RootState) => state.network.status);
15
+ const qaStatus = useSelector((state: RootState) => state.qa.status);
16
+
17
+ useEffect(() => {
18
+ const handleNewUpdate = (event: Event) => {
19
+ const customEvent = event as CustomEvent<NewAnswerEvent>;
20
+
21
+ const customEventSource = eventSource as ExtendedEventSource;
22
+ if (customEventSource?.sendMessage) {
23
+ customEventSource.sendMessage(customEvent.detail);
24
+ }
25
+ };
26
+
27
+ window.addEventListener('newUpdate', handleNewUpdate);
28
+
29
+ return () => {
30
+ window.removeEventListener('newUpdate', handleNewUpdate);
31
+ };
32
+ }, []);
33
+
34
+ return (
35
+ <div className="ui-test-bar">
36
+ <button
37
+ type="button"
38
+ onClick={() => {
39
+ dispatch({
40
+ type: 'network/networkChangeStatusTo',
41
+ payload: {
42
+ status: networkStatus === 'offline' ? 'online' : 'offline',
43
+ },
44
+ });
45
+ }}
46
+ >
47
+ Change Network status to{' '}
48
+ {networkStatus === 'offline' ? 'online' : 'offline'}
49
+ </button>
50
+ <button
51
+ type="button"
52
+ onClick={() => {
53
+ dispatch({
54
+ type: `qa/${qaStatus === 'live' ? 'close' : 'open'}`,
55
+ payload: null,
56
+ });
57
+ }}
58
+ >
59
+ Change the status to {qaStatus === 'live' ? 'Close' : 'Live'}
60
+ </button>
61
+ <button
62
+ type="button"
63
+ onClick={() => {
64
+ const event = new CustomEvent('newUpdate', {
65
+ detail: newAnserEventData(),
66
+ });
67
+ window.dispatchEvent(event);
68
+ }}
69
+ >
70
+ Send a real-time update
71
+ </button>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ export default UITest;
@@ -0,0 +1,45 @@
1
+ import { useContext } from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+ import { AppDispatch } from '../store';
4
+ import { useUpdatedComments } from '../services/comments-api';
5
+ import { updateComments } from '../store/updateComments';
6
+ import { QandaContext } from './QandaProvider';
7
+
8
+ import type { Comment } from '../types/comment';
9
+
10
+ function UpdatesButton() {
11
+ const { storyId, useStaging, commentsAPIUrl } = useContext(QandaContext);
12
+ const dispatch = useDispatch<AppDispatch>();
13
+ const updatedComments: Comment[] = useUpdatedComments({
14
+ storyId,
15
+ useStaging,
16
+ commentsAPIUrl,
17
+ });
18
+
19
+ const handleShowUpdates = () => {
20
+ updateComments(
21
+ dispatch,
22
+ updatedComments,
23
+ storyId,
24
+ useStaging,
25
+ commentsAPIUrl,
26
+ );
27
+ };
28
+
29
+ return (
30
+ <>
31
+ {updatedComments && updatedComments.length && (
32
+ <button
33
+ data-testid="qanda-button"
34
+ className="update-button"
35
+ onClick={() => handleShowUpdates()}
36
+ type="button"
37
+ >
38
+ {`There are ${updatedComments.length} updates available`}
39
+ </button>
40
+ )}
41
+ </>
42
+ );
43
+ }
44
+
45
+ export default UpdatesButton;
@@ -0,0 +1,57 @@
1
+ import { useContext } from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import { RootState } from '../store';
4
+ import { useGetUserRolesQuery } from '../services/comments-api';
5
+ import { QandaContext } from './QandaProvider';
6
+
7
+ function UserInfo() {
8
+ const profile = useSelector((state: RootState) => state.user.profile);
9
+ if (!profile) {
10
+ return null;
11
+ }
12
+
13
+ const { storyId, useStaging, commentsAPIUrl } = useContext(QandaContext);
14
+
15
+ const {
16
+ data: userRole,
17
+ error,
18
+ isLoading,
19
+ } = useGetUserRolesQuery(
20
+ {
21
+ storyId,
22
+ useStaging,
23
+ commentsAPIUrl,
24
+ token: profile.token,
25
+ },
26
+ { skip: !profile.token },
27
+ );
28
+
29
+ return (
30
+ <div>
31
+ {isLoading && <p>Loading your profile...</p>}
32
+ {error && (
33
+ <p>
34
+ <b>{error}</b>
35
+ </p>
36
+ )}
37
+ {userRole && JSON.stringify(userRole)}
38
+ {userRole?.isExpert && (
39
+ <p>
40
+ <b>It seems you are a bit of an expert:</b>{' '}
41
+ {userRole.isStaffExpert ? 'FT Reporter' : 'FT Expert'}
42
+ </p>
43
+ )}
44
+ {profile?.token ? (
45
+ <p>
46
+ <b>{`Your display name is ${profile.displayName}`}</b>
47
+ </p>
48
+ ) : (
49
+ <p>
50
+ <b>You are not logged in</b>
51
+ </p>
52
+ )}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export default UserInfo;
package/src/index.tsx ADDED
@@ -0,0 +1,58 @@
1
+ import { render, h } from 'preact';
2
+ import Qanda from './components/Qanda';
3
+ import type { QandaProps } from './types/qanda';
4
+
5
+ export class QandaUI {
6
+ containerElement: HTMLElement;
7
+ storyId: string;
8
+ useStaging: boolean;
9
+ title: string;
10
+ commentsAPIHost: string;
11
+ commentsAPIVersion: string;
12
+
13
+ constructor(containerElement: HTMLElement, options?: QandaProps) {
14
+ this.containerElement = containerElement;
15
+ if (!options && !containerElement) {
16
+ throw new Error('Required properties are missing');
17
+ }
18
+ const storyId = options?.storyId ?? containerElement.dataset.qandaStoryid;
19
+ const useStaging =
20
+ options?.useStaging ??
21
+ containerElement.dataset.qandaUsestaging === 'true' ??
22
+ false;
23
+ const title = options?.title ?? containerElement.dataset.qandaTitle;
24
+ const commentsAPIHost =
25
+ options?.commentsAPIHost ?? containerElement.dataset.qandaCommentsapihost;
26
+ const commentsAPIVersion =
27
+ options?.commentsAPIVersion ??
28
+ containerElement.dataset.qandaCommentsapiversion;
29
+
30
+ if (!storyId || !title || !commentsAPIHost || !commentsAPIVersion) {
31
+ throw new Error('Required properties are missing');
32
+ }
33
+ //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
34
+ this.storyId = storyId;
35
+ this.useStaging = useStaging;
36
+ this.title = title;
37
+ this.commentsAPIHost = commentsAPIHost;
38
+ this.commentsAPIVersion = commentsAPIVersion;
39
+ }
40
+
41
+ init() {
42
+ render(
43
+ <Qanda
44
+ storyId={this.storyId}
45
+ useStaging={this.useStaging}
46
+ title={this.title}
47
+ commentsAPIHost={this.commentsAPIHost}
48
+ commentsAPIVersion={this.commentsAPIVersion}
49
+ />,
50
+
51
+ this.containerElement,
52
+ );
53
+ }
54
+
55
+ destroy() {
56
+ render(null, this.containerElement);
57
+ }
58
+ }